From 579cbe5013fd2e8a947a2888184bf82272dc2d45 Mon Sep 17 00:00:00 2001 From: "Kenny (Knight) Sheridan" Date: Mon, 29 Dec 2025 20:56:45 -0800 Subject: [PATCH 1/8] added docs for Netbox enhancements --- docs/CPU_ENHANCEMENTS.md | 936 ++++++++++++++++++++++++++++ docs/ENHANCEMENTS.md | 541 ++++++++++++++++ docs/GPU_DETECTION.md | 955 ++++++++++++++++++++++++++++ docs/MEMORY_ENHANCEMENTS.md | 662 ++++++++++++++++++++ docs/NETWORK_ENHANCEMENTS.md | 620 +++++++++++++++++++ docs/RUSTDOC_STANDARDS.md | 560 +++++++++++++++++ docs/STORAGE_DETECTION.md | 1136 ++++++++++++++++++++++++++++++++++ docs/TESTING_STRATEGY.md | 709 +++++++++++++++++++++ 8 files changed, 6119 insertions(+) create mode 100644 docs/CPU_ENHANCEMENTS.md create mode 100644 docs/ENHANCEMENTS.md create mode 100644 docs/GPU_DETECTION.md create mode 100644 docs/MEMORY_ENHANCEMENTS.md create mode 100644 docs/NETWORK_ENHANCEMENTS.md create mode 100644 docs/RUSTDOC_STANDARDS.md create mode 100644 docs/STORAGE_DETECTION.md create mode 100644 docs/TESTING_STRATEGY.md diff --git a/docs/CPU_ENHANCEMENTS.md b/docs/CPU_ENHANCEMENTS.md new file mode 100644 index 0000000..f95269e --- /dev/null +++ b/docs/CPU_ENHANCEMENTS.md @@ -0,0 +1,936 @@ +# CPU Enhancement Plan + +> **Category:** Critical Issue +> **Target Platforms:** Linux (x86_64, aarch64) +> **Priority:** Critical - CPU frequency not exposed, cache sizes missing + +## Table of Contents + +1. [Problem Statement](#problem-statement) +2. [Current Implementation](#current-implementation) +3. [Multi-Method Detection Strategy](#multi-method-detection-strategy) +4. [Entity Changes](#entity-changes) +5. [Detection Method Details](#detection-method-details) +6. [Adapter Implementation](#adapter-implementation) +7. [Parser Implementation](#parser-implementation) +8. [Architecture-Specific Handling](#architecture-specific-handling) +9. [Testing Requirements](#testing-requirements) +10. [References](#references) + +--- + +## Problem Statement + +### Current Issue + +The `CpuInfo` structure lacks critical fields for CMDB inventory: + +```rust +// Current struct - limited fields +pub struct CpuInfo { + pub model: String, + pub cores: u32, + pub threads: u32, + pub sockets: u32, + pub speed: String, // String format, unreliable +} +``` + +Issues: +1. **Frequency as String** - `speed: "2300.000 MHz"` cannot be parsed reliably +2. **No cache information** - L1/L2/L3 cache sizes missing +3. **No architecture field** - Cannot distinguish x86_64 vs aarch64 +4. **No CPU flags** - Missing feature detection (AVX, SVE, etc.) + +### Impact + +- CMDB uses hardcoded 2100 MHz for CPU frequency +- Cannot assess cache hierarchy for performance analysis +- Cannot verify CPU features for workload compatibility + +### Requirements + +1. **Numeric frequency field** - `frequency_mhz: u32` +2. **Cache size fields** - L1d, L1i, L2, L3 in kilobytes +3. **Architecture detection** - x86_64, aarch64, etc. +4. **CPU flags/features** - Vector extensions, virtualization, etc. +5. **Multi-method detection** - sysfs, CPUID, lscpu, sysinfo + +--- + +## Current Implementation + +### Location + +- **Entity:** `src/domain/entities.rs:163-175` +- **Adapter:** `src/adapters/secondary/system/linux.rs:67-102` +- **Parser:** `src/domain/parsers/cpu.rs` + +### Current Detection Flow + +``` +┌─────────────────────────────────────────┐ +│ LinuxSystemInfoProvider::get_cpu_info() │ +└─────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ lscpu │ + └──────────────────┘ + │ + ▼ + ┌──────────────────────┐ + │ dmidecode -t processor│ + │ (requires privileges) │ + └──────────────────────┘ + │ + ▼ + Combine and return CpuInfo +``` + +### Current Limitations + +| Limitation | Impact | +|------------|--------| +| No sysfs reads | Misses cpufreq data | +| No CPUID access | Misses cache details on x86 | +| Speed as string | Consumer parsing issues | +| No cache info | Missing CMDB fields | +| No flags/features | Cannot verify capabilities | + +--- + +## Multi-Method Detection Strategy + +### Detection Priority Chain + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ CPU DETECTION CHAIN │ +│ │ +│ Priority 1: sysfs /sys/devices/system/cpu │ +│ ├── Most reliable for frequency and cache │ +│ ├── Works on all Linux architectures │ +│ ├── cpufreq for frequency │ +│ └── cache/index* for cache sizes │ +│ │ │ +│ ▼ (for x86 detailed cache info) │ +│ Priority 2: raw-cpuid crate (x86/x86_64 only) │ +│ ├── Direct CPUID instruction access │ +│ ├── Accurate cache line/associativity info │ +│ ├── CPU features and flags │ +│ └── Feature-gated: #[cfg(feature = "x86-cpu")] │ +│ │ │ +│ ▼ (for model and topology) │ +│ Priority 3: /proc/cpuinfo │ +│ ├── Model name │ +│ ├── Vendor │ +│ ├── Flags (x86) │ +│ └── Features (ARM) │ +│ │ │ +│ ▼ (for topology) │ +│ Priority 4: lscpu command │ +│ ├── Socket/core/thread topology │ +│ ├── NUMA information │ +│ └── Architecture detection │ +│ │ │ +│ ▼ (for SMBIOS data) │ +│ Priority 5: dmidecode -t processor │ +│ ├── Serial number (on some systems) │ +│ ├── Max frequency │ +│ └── Requires privileges │ +│ │ │ +│ ▼ (cross-platform fallback) │ +│ Priority 6: sysinfo crate │ +│ ├── Basic CPU info │ +│ ├── Cross-platform │ +│ └── Limited detail │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### Method Capabilities Matrix + +| Method | Frequency | Cache | Model | Vendor | Flags | Topology | Arch | +|--------|-----------|-------|-------|--------|-------|----------|------| +| sysfs | Yes | Yes | No | No | No | Partial | Yes | +| raw-cpuid | Yes | Yes | Yes | Yes | Yes | No | x86 only | +| /proc/cpuinfo | No | No | Yes | Yes | Yes | Partial | Yes | +| lscpu | Partial | Partial | Yes | Yes | Partial | Yes | Yes | +| dmidecode | Yes | No | Yes | Yes | No | Partial | Yes | +| sysinfo | Yes | No | Partial | No | No | Yes | Yes | + +--- + +## Entity Changes + +### New CpuInfo Structure + +```rust +// src/domain/entities.rs + +/// CPU cache level information +/// +/// Represents a single cache level (L1d, L1i, L2, L3). +/// +/// # References +/// +/// - [Intel CPUID](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) +/// - [Linux cache sysfs](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-system-cpu) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpuCacheInfo { + /// Cache level (1, 2, or 3) + pub level: u8, + + /// Cache type: "Data", "Instruction", or "Unified" + pub cache_type: String, + + /// Cache size in kilobytes + pub size_kb: u32, + + /// Number of ways of associativity + pub ways_of_associativity: Option, + + /// Cache line size in bytes + pub line_size_bytes: Option, + + /// Number of sets + pub sets: Option, + + /// Whether this cache is shared across cores + pub shared_cpu_map: Option, +} + +/// CPU information with extended details +/// +/// Provides comprehensive CPU information for CMDB inventory, +/// including frequency, cache hierarchy, and feature flags. +/// +/// # Detection Methods +/// +/// CPU information is gathered from multiple sources in priority order: +/// 1. **sysfs** - `/sys/devices/system/cpu` (frequency, cache) +/// 2. **raw-cpuid** - CPUID instruction (x86 only, cache details) +/// 3. **/proc/cpuinfo** - Model, vendor, flags +/// 4. **lscpu** - Topology information +/// 5. **dmidecode** - SMBIOS data (requires privileges) +/// 6. **sysinfo** - Cross-platform fallback +/// +/// # Frequency Values +/// +/// Multiple frequency values are provided: +/// - `frequency_mhz` - Current or maximum frequency (primary field) +/// - `frequency_min_mhz` - Minimum scaling frequency +/// - `frequency_max_mhz` - Maximum scaling frequency +/// - `frequency_base_mhz` - Base (non-turbo) frequency +/// +/// # Cache Hierarchy +/// +/// Cache sizes are provided per-core in kilobytes: +/// - `cache_l1d_kb` - L1 data cache +/// - `cache_l1i_kb` - L1 instruction cache +/// - `cache_l2_kb` - L2 cache (may be per-core or shared) +/// - `cache_l3_kb` - L3 cache (typically shared) +/// +/// # Example +/// +/// ``` +/// use hardware_report::CpuInfo; +/// +/// // Calculate total L3 cache +/// let total_l3_mb = cpu.cache_l3_kb.unwrap_or(0) as f64 / 1024.0; +/// +/// // Check for AVX-512 support +/// let has_avx512 = cpu.flags.iter().any(|f| f.starts_with("avx512")); +/// ``` +/// +/// # References +/// +/// - [Linux CPU sysfs](https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.rst) +/// - [Intel CPUID](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) +/// - [ARM CPU ID registers](https://developer.arm.com/documentation/ddi0487/latest) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpuInfo { + /// CPU model name + /// + /// Examples: + /// - "AMD EPYC 7763 64-Core Processor" + /// - "Intel(R) Xeon(R) Platinum 8380 CPU @ 2.30GHz" + /// - "Neoverse-N1" + pub model: String, + + /// CPU vendor identifier + /// + /// Values: + /// - "GenuineIntel" (Intel) + /// - "AuthenticAMD" (AMD) + /// - "ARM" (ARM/Ampere/etc) + pub vendor: String, + + /// Number of physical cores per socket + pub cores: u32, + + /// Number of threads per core (hyperthreading/SMT) + pub threads: u32, + + /// Number of CPU sockets + pub sockets: u32, + + /// Total physical cores (cores * sockets) + pub total_cores: u32, + + /// Total logical CPUs (cores * threads * sockets) + pub total_threads: u32, + + /// CPU frequency in MHz (current or max) + /// + /// Primary frequency field. This is the most useful value for + /// general CMDB purposes. + pub frequency_mhz: u32, + + /// Minimum scaling frequency in MHz + /// + /// From cpufreq scaling_min_freq. + pub frequency_min_mhz: Option, + + /// Maximum scaling frequency in MHz + /// + /// From cpufreq scaling_max_freq. This is the turbo/boost frequency. + pub frequency_max_mhz: Option, + + /// Base (non-turbo) frequency in MHz + /// + /// From cpufreq base_frequency or CPUID. + pub frequency_base_mhz: Option, + + /// CPU architecture + /// + /// Values: "x86_64", "aarch64", "armv7l", etc. + pub architecture: String, + + /// CPU microarchitecture name + /// + /// Examples: + /// - "Zen3" (AMD) + /// - "Ice Lake" (Intel) + /// - "Neoverse N1" (ARM) + pub microarchitecture: Option, + + /// L1 data cache size in kilobytes (per core) + pub cache_l1d_kb: Option, + + /// L1 instruction cache size in kilobytes (per core) + pub cache_l1i_kb: Option, + + /// L2 cache size in kilobytes (per core typically) + pub cache_l2_kb: Option, + + /// L3 cache size in kilobytes (typically shared) + /// + /// Note: This is often the total L3 across all cores in a socket. + pub cache_l3_kb: Option, + + /// Detailed cache information for each level + pub caches: Vec, + + /// CPU flags/features + /// + /// Examples (x86): "avx", "avx2", "avx512f", "aes", "sse4_2" + /// Examples (ARM): "fp", "asimd", "sve", "sve2" + /// + /// # References + /// + /// - [x86 CPUID flags](https://en.wikipedia.org/wiki/CPUID) + /// - [ARM HWCAP](https://www.kernel.org/doc/html/latest/arm64/elf_hwcaps.html) + pub flags: Vec, + + /// Microcode/firmware version + pub microcode_version: Option, + + /// CPU stepping (revision level) + pub stepping: Option, + + /// CPU family number + pub family: Option, + + /// CPU model number (not the name) + pub model_number: Option, + + /// Virtualization support + /// + /// Values: "VT-x", "AMD-V", "none", etc. + pub virtualization: Option, + + /// NUMA nodes count + pub numa_nodes: u32, + + /// Detection methods used + pub detection_methods: Vec, +} + +impl Default for CpuInfo { + fn default() -> Self { + Self { + model: String::new(), + vendor: String::new(), + cores: 0, + threads: 1, + sockets: 1, + total_cores: 0, + total_threads: 0, + frequency_mhz: 0, + frequency_min_mhz: None, + frequency_max_mhz: None, + frequency_base_mhz: None, + architecture: std::env::consts::ARCH.to_string(), + microarchitecture: None, + cache_l1d_kb: None, + cache_l1i_kb: None, + cache_l2_kb: None, + cache_l3_kb: None, + caches: Vec::new(), + flags: Vec::new(), + microcode_version: None, + stepping: None, + family: None, + model_number: None, + virtualization: None, + numa_nodes: 1, + detection_methods: Vec::new(), + } + } +} +``` + +--- + +## Detection Method Details + +### Method 1: sysfs /sys/devices/system/cpu + +**When:** Linux systems (always primary for freq/cache) + +**sysfs paths:** + +``` +/sys/devices/system/cpu/ +├── cpu0/ +│ ├── cpufreq/ +│ │ ├── cpuinfo_max_freq # Max frequency in kHz +│ │ ├── cpuinfo_min_freq # Min frequency in kHz +│ │ ├── scaling_cur_freq # Current frequency in kHz +│ │ ├── scaling_max_freq # Scaling max in kHz +│ │ ├── scaling_min_freq # Scaling min in kHz +│ │ └── base_frequency # Base (non-turbo) freq +│ ├── cache/ +│ │ ├── index0/ # L1d typically +│ │ │ ├── level # Cache level (1, 2, 3) +│ │ │ ├── type # Data, Instruction, Unified +│ │ │ ├── size # Size with unit (e.g., "32K") +│ │ │ ├── ways_of_associativity +│ │ │ ├── coherency_line_size +│ │ │ └── number_of_sets +│ │ ├── index1/ # L1i typically +│ │ ├── index2/ # L2 typically +│ │ └── index3/ # L3 typically +│ └── topology/ +│ ├── physical_package_id # Socket ID +│ ├── core_id # Core ID within socket +│ └── thread_siblings_list # SMT siblings +├── possible # Possible CPU range +├── present # Present CPU range +└── online # Online CPU range +``` + +**Frequency parsing:** + +```rust +// sysfs reports frequency in kHz, convert to MHz +let freq_khz: u32 = read_sysfs("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq")? + .trim() + .parse()?; +let freq_mhz = freq_khz / 1000; +``` + +**Cache size parsing:** + +```rust +// sysfs reports size as "32K", "512K", "32768K", "16M", etc. +fn parse_cache_size(size_str: &str) -> Option { + let s = size_str.trim(); + if s.ends_with('K') { + s[..s.len()-1].parse().ok() + } else if s.ends_with('M') { + s[..s.len()-1].parse::().ok().map(|v| v * 1024) + } else { + s.parse().ok() + } +} +``` + +**References:** +- [CPU sysfs Documentation](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-system-cpu) +- [cpufreq User Guide](https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.rst) + +--- + +### Method 2: raw-cpuid (x86/x86_64 only) + +**When:** x86/x86_64 architecture, feature enabled + +**Cargo.toml:** +```toml +[target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies] +raw-cpuid = { version = "11", optional = true } + +[features] +x86-cpu = ["raw-cpuid"] +``` + +**Usage:** +```rust +#[cfg(all(feature = "x86-cpu", any(target_arch = "x86", target_arch = "x86_64")))] +fn get_cpu_info_cpuid() -> CpuInfo { + use raw_cpuid::CpuId; + + let cpuid = CpuId::new(); + + let model = cpuid.get_processor_brand_string() + .map(|b| b.as_str().trim().to_string()) + .unwrap_or_default(); + + let vendor = cpuid.get_vendor_info() + .map(|v| v.as_str().to_string()) + .unwrap_or_default(); + + // Cache info + if let Some(cache_params) = cpuid.get_cache_parameters() { + for cache in cache_params { + let size_kb = (cache.associativity() + * cache.physical_line_partitions() + * cache.coherency_line_size() + * cache.sets()) as u32 / 1024; + // ... + } + } + + // Feature flags + if let Some(features) = cpuid.get_feature_info() { + // Check SSE, AVX, etc. + } + + // ... +} +``` + +**References:** +- [raw-cpuid crate](https://docs.rs/raw-cpuid) +- [Intel CPUID Reference](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) + +--- + +### Method 3: /proc/cpuinfo + +**When:** Linux, for model name and flags + +**Path:** `/proc/cpuinfo` + +**Format (x86):** +``` +processor : 0 +vendor_id : GenuineIntel +cpu family : 6 +model : 106 +model name : Intel(R) Xeon(R) Platinum 8380 CPU @ 2.30GHz +stepping : 6 +microcode : 0xd0003a5 +cpu MHz : 2300.000 +cache size : 61440 KB +flags : fpu vme de pse ... avx avx2 avx512f avx512dq ... +``` + +**Format (ARM):** +``` +processor : 0 +BogoMIPS : 50.00 +Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 ... +CPU implementer : 0x41 +CPU architecture: 8 +CPU variant : 0x3 +CPU part : 0xd0c +CPU revision : 1 +``` + +**References:** +- [/proc/cpuinfo](https://man7.org/linux/man-pages/man5/proc.5.html) + +--- + +### Method 4: lscpu + +**When:** For topology information + +**Command:** +```bash +lscpu -J # JSON output (preferred) +lscpu # Text output (fallback) +``` + +**JSON output:** +```json +{ + "lscpu": [ + {"field": "Architecture:", "data": "x86_64"}, + {"field": "CPU(s):", "data": "128"}, + {"field": "Thread(s) per core:", "data": "2"}, + {"field": "Core(s) per socket:", "data": "32"}, + {"field": "Socket(s):", "data": "2"}, + {"field": "NUMA node(s):", "data": "2"}, + {"field": "Model name:", "data": "AMD EPYC 7763 64-Core Processor"}, + {"field": "CPU max MHz:", "data": "3500.0000"}, + {"field": "L1d cache:", "data": "2 MiB"}, + {"field": "L1i cache:", "data": "2 MiB"}, + {"field": "L2 cache:", "data": "32 MiB"}, + {"field": "L3 cache:", "data": "256 MiB"} + ] +} +``` + +**References:** +- [lscpu man page](https://man7.org/linux/man-pages/man1/lscpu.1.html) + +--- + +### Method 5: sysinfo crate + +**When:** Cross-platform fallback + +**Usage:** +```rust +use sysinfo::System; + +let sys = System::new_all(); + +let frequency = sys.cpus().first() + .map(|cpu| cpu.frequency() as u32) + .unwrap_or(0); + +let physical_cores = sys.physical_core_count().unwrap_or(0); +let logical_cpus = sys.cpus().len(); +``` + +**References:** +- [sysinfo crate](https://docs.rs/sysinfo) + +--- + +## Architecture-Specific Handling + +### x86_64 + +```rust +#[cfg(target_arch = "x86_64")] +fn detect_cpu_x86(info: &mut CpuInfo) { + // Use raw-cpuid if available + #[cfg(feature = "x86-cpu")] + { + use raw_cpuid::CpuId; + let cpuid = CpuId::new(); + // Extract cache, features, etc. + } + + // Read flags from /proc/cpuinfo + // Parse "flags" line +} +``` + +### aarch64 + +```rust +#[cfg(target_arch = "aarch64")] +fn detect_cpu_arm(info: &mut CpuInfo) { + // Read from /proc/cpuinfo + // "Features" line instead of "flags" + // "CPU part" for microarchitecture detection + + // Map CPU part to microarchitecture + // 0xd0c -> "Neoverse N1" + // 0xd40 -> "Neoverse V1" + // etc. +} + +/// Map ARM CPU part ID to microarchitecture name +/// +/// # References +/// +/// - [ARM CPU Part Numbers](https://developer.arm.com/documentation/ddi0487/latest) +fn arm_cpu_part_to_name(part: &str) -> Option<&'static str> { + match part.to_lowercase().as_str() { + "0xd03" => Some("Cortex-A53"), + "0xd07" => Some("Cortex-A57"), + "0xd08" => Some("Cortex-A72"), + "0xd09" => Some("Cortex-A73"), + "0xd0a" => Some("Cortex-A75"), + "0xd0b" => Some("Cortex-A76"), + "0xd0c" => Some("Neoverse N1"), + "0xd0d" => Some("Cortex-A77"), + "0xd40" => Some("Neoverse V1"), + "0xd41" => Some("Cortex-A78"), + "0xd44" => Some("Cortex-X1"), + "0xd49" => Some("Neoverse N2"), + "0xd4f" => Some("Neoverse V2"), + _ => None, + } +} +``` + +--- + +## Parser Implementation + +### File: `src/domain/parsers/cpu.rs` + +```rust +//! CPU information parsing functions +//! +//! This module provides pure parsing functions for CPU information from +//! various sources. All functions take string input and return parsed +//! results without performing I/O. +//! +//! # Supported Formats +//! +//! - sysfs frequency/cache files +//! - /proc/cpuinfo +//! - lscpu text and JSON output +//! - dmidecode processor output +//! +//! # References +//! +//! - [Linux CPU sysfs](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-system-cpu) +//! - [/proc/cpuinfo](https://man7.org/linux/man-pages/man5/proc.5.html) + +use crate::domain::{CpuCacheInfo, CpuInfo}; + +/// Parse sysfs frequency file (in kHz) to MHz +/// +/// # Arguments +/// +/// * `content` - Content of cpufreq file (e.g., scaling_max_freq) +/// +/// # Returns +/// +/// Frequency in MHz. +/// +/// # Example +/// +/// ``` +/// use hardware_report::domain::parsers::cpu::parse_sysfs_freq_khz; +/// +/// assert_eq!(parse_sysfs_freq_khz("3500000").unwrap(), 3500); +/// ``` +pub fn parse_sysfs_freq_khz(content: &str) -> Result { + let khz: u32 = content.trim().parse() + .map_err(|e| format!("Invalid frequency: {}", e))?; + Ok(khz / 1000) +} + +/// Parse sysfs cache size (e.g., "32K", "1M") +/// +/// # Arguments +/// +/// * `content` - Content of cache size file +/// +/// # Returns +/// +/// Size in kilobytes. +/// +/// # Example +/// +/// ``` +/// use hardware_report::domain::parsers::cpu::parse_sysfs_cache_size; +/// +/// assert_eq!(parse_sysfs_cache_size("32K").unwrap(), 32); +/// assert_eq!(parse_sysfs_cache_size("1M").unwrap(), 1024); +/// assert_eq!(parse_sysfs_cache_size("256M").unwrap(), 262144); +/// ``` +pub fn parse_sysfs_cache_size(content: &str) -> Result { + let s = content.trim(); + if s.ends_with('K') { + s[..s.len()-1].parse() + .map_err(|e| format!("Invalid cache size: {}", e)) + } else if s.ends_with('M') { + s[..s.len()-1].parse::() + .map(|v| v * 1024) + .map_err(|e| format!("Invalid cache size: {}", e)) + } else if s.ends_with('G') { + s[..s.len()-1].parse::() + .map(|v| v * 1024 * 1024) + .map_err(|e| format!("Invalid cache size: {}", e)) + } else { + s.parse() + .map_err(|e| format!("Invalid cache size: {}", e)) + } +} + +/// Parse /proc/cpuinfo content +/// +/// # Arguments +/// +/// * `content` - Full content of /proc/cpuinfo +/// +/// # Returns +/// +/// Partial CpuInfo with fields from cpuinfo. +/// +/// # References +/// +/// - [/proc filesystem](https://man7.org/linux/man-pages/man5/proc.5.html) +pub fn parse_proc_cpuinfo(content: &str) -> Result { + let mut info = CpuInfo::default(); + + for line in content.lines() { + let parts: Vec<&str> = line.splitn(2, ':').collect(); + if parts.len() != 2 { + continue; + } + + let key = parts[0].trim(); + let value = parts[1].trim(); + + match key { + "model name" => info.model = value.to_string(), + "vendor_id" => info.vendor = value.to_string(), + "cpu family" => info.family = value.parse().ok(), + "model" => info.model_number = value.parse().ok(), + "stepping" => info.stepping = value.parse().ok(), + "microcode" => info.microcode_version = Some(value.to_string()), + "flags" | "Features" => { + info.flags = value.split_whitespace() + .map(String::from) + .collect(); + } + "CPU implementer" => { + if info.vendor.is_empty() { + info.vendor = "ARM".to_string(); + } + } + "CPU part" => { + // ARM CPU part number + if let Some(arch) = arm_cpu_part_to_name(value) { + info.microarchitecture = Some(arch.to_string()); + } + } + _ => {} + } + } + + Ok(info) +} + +/// Parse lscpu JSON output +/// +/// # Arguments +/// +/// * `output` - JSON output from `lscpu -J` +/// +/// # References +/// +/// - [lscpu](https://man7.org/linux/man-pages/man1/lscpu.1.html) +pub fn parse_lscpu_json(output: &str) -> Result { + todo!() +} + +/// Parse lscpu text output +/// +/// # Arguments +/// +/// * `output` - Text output from `lscpu` +pub fn parse_lscpu_text(output: &str) -> Result { + todo!() +} + +/// Map ARM CPU part ID to microarchitecture name +/// +/// # Arguments +/// +/// * `part` - CPU part from /proc/cpuinfo (e.g., "0xd0c") +/// +/// # References +/// +/// - [ARM CPU Part Numbers](https://developer.arm.com/documentation/ddi0487/latest) +pub fn arm_cpu_part_to_name(part: &str) -> Option<&'static str> { + match part.to_lowercase().as_str() { + "0xd03" => Some("Cortex-A53"), + "0xd07" => Some("Cortex-A57"), + "0xd08" => Some("Cortex-A72"), + "0xd09" => Some("Cortex-A73"), + "0xd0a" => Some("Cortex-A75"), + "0xd0b" => Some("Cortex-A76"), + "0xd0c" => Some("Neoverse N1"), + "0xd0d" => Some("Cortex-A77"), + "0xd40" => Some("Neoverse V1"), + "0xd41" => Some("Cortex-A78"), + "0xd44" => Some("Cortex-X1"), + "0xd49" => Some("Neoverse N2"), + "0xd4f" => Some("Neoverse V2"), + "0xd80" => Some("Cortex-A520"), + "0xd81" => Some("Cortex-A720"), + _ => None, + } +} +``` + +--- + +## Testing Requirements + +### Unit Tests + +| Test | Description | +|------|-------------| +| `test_parse_sysfs_freq` | Parse frequency in kHz to MHz | +| `test_parse_cache_size` | Parse K/M/G suffixes | +| `test_parse_proc_cpuinfo_intel` | Parse Intel cpuinfo | +| `test_parse_proc_cpuinfo_amd` | Parse AMD cpuinfo | +| `test_parse_proc_cpuinfo_arm` | Parse ARM cpuinfo | +| `test_parse_lscpu_json` | Parse lscpu JSON | +| `test_arm_cpu_part_mapping` | ARM part to name | + +### Integration Tests + +| Test | Platform | Description | +|------|----------|-------------| +| `test_cpu_detection_x86` | x86_64 | Full detection on x86 | +| `test_cpu_detection_arm` | aarch64 | Full detection on ARM | +| `test_cpuid_features` | x86_64 | raw-cpuid feature detection | +| `test_sysfs_cache` | Linux | sysfs cache reading | + +--- + +## References + +### Official Documentation + +| Resource | URL | +|----------|-----| +| Linux CPU sysfs | https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-system-cpu | +| Linux cpufreq | https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.rst | +| Intel CPUID | https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html | +| AMD CPUID | https://www.amd.com/en/support/tech-docs | +| ARM CPU ID | https://developer.arm.com/documentation/ddi0487/latest | +| ARM HWCAP | https://www.kernel.org/doc/html/latest/arm64/elf_hwcaps.html | + +### Crate Documentation + +| Crate | URL | +|-------|-----| +| raw-cpuid | https://docs.rs/raw-cpuid | +| sysinfo | https://docs.rs/sysinfo | + +--- + +## Changelog + +| Date | Changes | +|------|---------| +| 2024-12-29 | Initial specification | diff --git a/docs/ENHANCEMENTS.md b/docs/ENHANCEMENTS.md new file mode 100644 index 0000000..052becd --- /dev/null +++ b/docs/ENHANCEMENTS.md @@ -0,0 +1,541 @@ +# Hardware Report Enhancement Plan + +> **Version:** 0.2.0 +> **Target:** Linux (primary), macOS (secondary) +> **Architecture Focus:** x86_64, aarch64/ARM64 + +## Table of Contents + +1. [Overview](#overview) +2. [Architecture Principles](#architecture-principles) +3. [Enhancement Summary](#enhancement-summary) +4. [Phase 1: Critical Issues](#phase-1-critical-issues) +5. [Phase 2: Data Gaps](#phase-2-data-gaps) +6. [Phase 3: Runtime Metrics](#phase-3-runtime-metrics) +7. [New Dependencies](#new-dependencies) +8. [Implementation Order](#implementation-order) +9. [Related Documents](#related-documents) + +--- + +## Overview + +This document outlines the implementation plan for enhancing the `hardware_report` crate to better serve CMDB (Configuration Management Database) inventory use cases, specifically addressing issues encountered in the `metal-agent` project. + +### Goals + +1. **Eliminate fallback collection methods** - Provide complete, accurate data natively +2. **Multi-architecture support** - Full functionality on x86_64 and aarch64 (ARM64) +3. **Numeric data formats** - Return parseable numeric values, not formatted strings +4. **Multi-method detection** - Use multiple detection strategies with graceful fallbacks +5. **Comprehensive documentation** - Rustdoc for all public APIs with links to official references + +### Non-Goals + +- Windows support (out of scope for this phase) +- Real-time monitoring (basic runtime metrics only) +- Container/VM detection improvements + +--- + +## Architecture Principles + +This implementation strictly follows the **Hexagonal Architecture (Ports and Adapters)** pattern already established in the codebase. + +### Layer Responsibilities + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ DOMAIN LAYER │ +│ src/domain/ │ +│ ├── entities.rs # Data structures (platform-agnostic) │ +│ ├── errors.rs # Domain errors │ +│ ├── parsers/ # Pure parsing functions (no I/O) │ +│ │ ├── cpu.rs │ +│ │ ├── memory.rs │ +│ │ ├── storage.rs │ +│ │ ├── network.rs │ +│ │ └── gpu.rs # NEW │ +│ └── services/ # Domain services (orchestration) │ +└─────────────────────────────────────────────────────────────────────┘ + │ + │ implements + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ PORTS LAYER │ +│ src/ports/ │ +│ ├── primary/ # Offered interfaces (what we provide) │ +│ │ └── reporting.rs # HardwareReportingService trait │ +│ └── secondary/ # Required interfaces (what we need) │ +│ ├── system.rs # SystemInfoProvider trait │ +│ ├── command.rs # CommandExecutor trait │ +│ └── publisher.rs # DataPublisher trait │ +└─────────────────────────────────────────────────────────────────────┘ + │ + │ implemented by + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ ADAPTERS LAYER │ +│ src/adapters/secondary/ │ +│ ├── system/ │ +│ │ ├── linux.rs # LinuxSystemInfoProvider │ +│ │ └── macos.rs # MacOSSystemInfoProvider │ +│ ├── command/ │ +│ │ └── unix.rs # UnixCommandExecutor │ +│ └── publisher/ │ +│ ├── http.rs # HttpDataPublisher │ +│ └── file.rs # FileDataPublisher │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Key Principles + +| Principle | Description | +|-----------|-------------| +| **Domain Independence** | Domain layer has NO knowledge of adapters or I/O | +| **Pure Parsers** | Parsing functions take strings, return Results - no side effects | +| **Trait Abstraction** | All platform-specific code behind trait interfaces | +| **Multi-Method Detection** | Each adapter tries multiple methods, returns best result | +| **Graceful Degradation** | Partial data is better than no data - always return something | + +--- + +## Enhancement Summary + +### Critical Issues (Phase 1) + +| Issue | Impact | Solution | Doc | +|-------|--------|----------|-----| +| GPU memory returns unparseable string | CMDB shows 0MB VRAM | Numeric fields + multi-method detection | [GPU_DETECTION.md](./GPU_DETECTION.md) | +| Storage empty on ARM/aarch64 | No storage inventory | sysfs + sysinfo fallback chain | [STORAGE_DETECTION.md](./STORAGE_DETECTION.md) | +| CPU frequency not exposed | Hardcoded values | sysfs + raw-cpuid | [CPU_ENHANCEMENTS.md](./CPU_ENHANCEMENTS.md) | + +### Data Gaps (Phase 2) + +| Missing Field | Category | Priority | Doc | +|---------------|----------|----------|-----| +| CPU cache sizes (L1/L2/L3) | CPU | Medium | [CPU_ENHANCEMENTS.md](./CPU_ENHANCEMENTS.md) | +| DIMM part_number | Memory | Medium | [MEMORY_ENHANCEMENTS.md](./MEMORY_ENHANCEMENTS.md) | +| Storage serial_number | Storage | High | [STORAGE_DETECTION.md](./STORAGE_DETECTION.md) | +| Storage firmware_version | Storage | Medium | [STORAGE_DETECTION.md](./STORAGE_DETECTION.md) | +| GPU driver_version | GPU | High | [GPU_DETECTION.md](./GPU_DETECTION.md) | +| Network driver_version | Network | Low | [NETWORK_ENHANCEMENTS.md](./NETWORK_ENHANCEMENTS.md) | + +--- + +## Phase 1: Critical Issues + +### 1.1 GPU Detection Overhaul + +**Problem:** GPU memory returned as `"80 GB"` string, consumers can't parse numeric values. + +**Solution:** Multi-method detection with numeric output fields. + +See: [GPU_DETECTION.md](./GPU_DETECTION.md) + +#### Entity Changes + +```rust +// src/domain/entities.rs + +/// GPU device information +/// +/// Represents a discrete or integrated GPU detected in the system. +/// Memory values are provided in megabytes as unsigned integers for +/// reliable parsing by CMDB consumers. +/// +/// # Detection Methods +/// +/// GPUs are detected using multiple methods in priority order: +/// 1. NVML (NVIDIA Management Library) - most accurate for NVIDIA GPUs +/// 2. nvidia-smi command - fallback for NVIDIA when NVML unavailable +/// 3. ROCm SMI - AMD GPU detection +/// 4. sysfs /sys/class/drm - Linux DRM subsystem +/// 5. lspci - PCI device enumeration +/// 6. sysinfo crate - cross-platform fallback +/// +/// # References +/// +/// - [NVIDIA NVML Documentation](https://developer.nvidia.com/nvidia-management-library-nvml) +/// - [Linux DRM Subsystem](https://www.kernel.org/doc/html/latest/gpu/drm-uapi.html) +/// - [PCI ID Database](https://pci-ids.ucw.cz/) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GpuDevice { + /// GPU index (0-based) + pub index: u32, + + /// GPU product name (e.g., "NVIDIA H100 80GB HBM3") + pub name: String, + + /// GPU UUID (unique identifier) + pub uuid: String, + + /// Total GPU memory in megabytes + /// + /// This replaces the previous `memory: String` field which returned + /// formatted strings like "80 GB" that were difficult to parse. + pub memory_total_mb: u64, + + /// Available GPU memory in megabytes (runtime value, may be None if not queryable) + pub memory_free_mb: Option, + + /// GPU memory as formatted string (deprecated, for backward compatibility) + #[deprecated(since = "0.2.0", note = "Use memory_total_mb instead")] + #[serde(skip_serializing_if = "Option::is_none")] + pub memory: Option, + + /// PCI ID in format "vendor:device" (e.g., "10de:2330") + pub pci_id: String, + + /// PCI bus address (e.g., "0000:01:00.0") + pub pci_bus_id: Option, + + /// Vendor name (e.g., "NVIDIA", "AMD", "Intel") + pub vendor: String, + + /// Driver version (e.g., "535.129.03") + pub driver_version: Option, + + /// CUDA compute capability for NVIDIA GPUs (e.g., "9.0") + pub compute_capability: Option, + + /// GPU architecture (e.g., "Hopper", "Ada Lovelace", "RDNA3") + pub architecture: Option, + + /// NUMA node affinity (-1 if not applicable) + pub numa_node: Option, + + /// Detection method used to discover this GPU + pub detection_method: String, +} +``` + +### 1.2 Storage Detection on ARM + +**Problem:** `lsblk` returns empty on some ARM platforms. + +**Solution:** Multi-method detection with sysfs as primary on Linux. + +See: [STORAGE_DETECTION.md](./STORAGE_DETECTION.md) + +#### Entity Changes + +```rust +// src/domain/entities.rs + +/// Storage device type classification +/// +/// # References +/// +/// - [Linux Block Device Documentation](https://www.kernel.org/doc/html/latest/block/index.html) +/// - [NVMe Specification](https://nvmexpress.org/specifications/) +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub enum StorageType { + /// NVMe solid-state drive + Nvme, + /// SATA/SAS solid-state drive + Ssd, + /// Hard disk drive (rotational) + Hdd, + /// Embedded MMC storage + Emmc, + /// Unknown or unclassified storage type + Unknown, +} + +/// Storage device information +/// +/// # Detection Methods +/// +/// Storage devices are detected using multiple methods in priority order: +/// 1. sysfs /sys/block - direct kernel interface (Linux) +/// 2. lsblk command - block device listing +/// 3. sysinfo crate - cross-platform fallback +/// 4. diskutil (macOS) +/// +/// # References +/// +/// - [Linux sysfs Block Devices](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-block) +/// - [SMART Attributes](https://en.wikipedia.org/wiki/S.M.A.R.T.) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct StorageDevice { + /// Device name (e.g., "nvme0n1", "sda") + pub name: String, + + /// Device type classification + pub device_type: StorageType, + + /// Legacy type field (deprecated) + #[deprecated(since = "0.2.0", note = "Use device_type instead")] + #[serde(skip_serializing_if = "Option::is_none")] + pub type_: Option, + + /// Device size in bytes + pub size_bytes: u64, + + /// Device size in gigabytes (convenience field) + pub size_gb: f64, + + /// Legacy size field as string (deprecated) + #[deprecated(since = "0.2.0", note = "Use size_bytes or size_gb instead")] + #[serde(skip_serializing_if = "Option::is_none")] + pub size: Option, + + /// Device model name + pub model: String, + + /// Device serial number (may require elevated privileges) + pub serial_number: Option, + + /// Device firmware version + pub firmware_version: Option, + + /// Interface type (e.g., "NVMe", "SATA", "SAS", "eMMC") + pub interface: String, + + /// Whether the device is rotational (true = HDD, false = SSD/NVMe) + pub is_rotational: bool, + + /// WWN (World Wide Name) if available + pub wwn: Option, + + /// Detection method used + pub detection_method: String, +} +``` + +### 1.3 CPU Frequency and Cache + +**Problem:** CPU frequency hardcoded, cache sizes not exposed. + +**Solution:** sysfs reads + raw-cpuid for x86. + +See: [CPU_ENHANCEMENTS.md](./CPU_ENHANCEMENTS.md) + +#### Entity Changes + +```rust +// src/domain/entities.rs + +/// CPU information with extended details +/// +/// # Detection Methods +/// +/// CPU information is gathered from multiple sources: +/// 1. sysfs /sys/devices/system/cpu - frequency and cache (Linux) +/// 2. /proc/cpuinfo - model and features (Linux) +/// 3. raw-cpuid crate - x86 CPUID instruction +/// 4. lscpu command - topology information +/// 5. dmidecode - SMBIOS data (requires privileges) +/// 6. sysinfo crate - cross-platform fallback +/// +/// # References +/// +/// - [Linux CPU sysfs Interface](https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.rst) +/// - [Intel CPUID Reference](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) +/// - [ARM CPU Identification](https://developer.arm.com/documentation/ddi0487/latest) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpuInfo { + /// CPU model name (e.g., "AMD EPYC 7763 64-Core Processor") + pub model: String, + + /// CPU vendor (e.g., "GenuineIntel", "AuthenticAMD", "ARM") + pub vendor: String, + + /// Number of physical cores per socket + pub cores: u32, + + /// Number of threads per core (hyperthreading) + pub threads: u32, + + /// Number of CPU sockets + pub sockets: u32, + + /// CPU frequency in MHz (current or max) + pub frequency_mhz: u32, + + /// Legacy speed field as string (deprecated) + #[deprecated(since = "0.2.0", note = "Use frequency_mhz instead")] + #[serde(skip_serializing_if = "Option::is_none")] + pub speed: Option, + + /// CPU architecture (e.g., "x86_64", "aarch64") + pub architecture: String, + + /// L1 data cache size in kilobytes (per core) + pub cache_l1d_kb: Option, + + /// L1 instruction cache size in kilobytes (per core) + pub cache_l1i_kb: Option, + + /// L2 cache size in kilobytes (per core or shared) + pub cache_l2_kb: Option, + + /// L3 cache size in kilobytes (typically shared) + pub cache_l3_kb: Option, + + /// CPU flags/features (e.g., "avx2", "sve") + pub flags: Vec, + + /// Microcode version + pub microcode_version: Option, +} +``` + +--- + +## Phase 2: Data Gaps + +### 2.1 Memory DIMM Part Number + +See: [MEMORY_ENHANCEMENTS.md](./MEMORY_ENHANCEMENTS.md) + +```rust +/// Individual memory module (DIMM) +/// +/// # References +/// +/// - [JEDEC Memory Standards](https://www.jedec.org/) +/// - [SMBIOS Type 17 Memory Device](https://www.dmtf.org/standards/smbios) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MemoryModule { + pub size: String, + pub size_bytes: u64, // NEW + pub type_: String, + pub speed: String, + pub speed_mhz: Option, // NEW + pub location: String, + pub manufacturer: String, + pub serial: String, + pub part_number: Option, // NEW + pub rank: Option, // NEW + pub configured_voltage: Option, // NEW (in volts) +} +``` + +### 2.2 Network Interface Enhancements + +See: [NETWORK_ENHANCEMENTS.md](./NETWORK_ENHANCEMENTS.md) + +```rust +/// Network interface information +/// +/// # References +/// +/// - [Linux Netlink Documentation](https://man7.org/linux/man-pages/man7/netlink.7.html) +/// - [ethtool Source](https://mirrors.edge.kernel.org/pub/software/network/ethtool/) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct NetworkInterface { + pub name: String, + pub mac: String, + pub ip: String, + pub prefix: String, + pub speed: Option, + pub speed_mbps: Option, // NEW + pub type_: String, + pub vendor: String, + pub model: String, + pub pci_id: String, + pub numa_node: Option, + pub driver: Option, // NEW + pub driver_version: Option, // NEW + pub firmware_version: Option, // NEW + pub mtu: u32, // NEW + pub is_up: bool, // NEW + pub is_virtual: bool, // NEW +} +``` + +--- + +## Phase 3: Runtime Metrics (Optional) + +These are lower priority and may be deferred: + +| Metric | Category | Notes | +|--------|----------|-------| +| GPU temperature | GPU | Requires NVML or sensors | +| GPU utilization | GPU | Requires NVML | +| GPU power draw | GPU | Requires NVML | +| Storage SMART data | Storage | Requires smartctl or sysfs | +| Network statistics | Network | /sys/class/net/*/statistics | + +--- + +## New Dependencies + +### Cargo.toml Changes + +```toml +[dependencies] +# Existing dependencies... +sysinfo = "0.32.0" + +# NEW: NVIDIA GPU detection via NVML +# Optional - requires NVIDIA driver at runtime +nvml-wrapper = { version = "0.9", optional = true } + +# NEW: x86 CPU detection via CPUID +# Only compiled on x86/x86_64 targets +[target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies] +raw-cpuid = { version = "11", optional = true } + +[features] +default = [] +nvidia = ["nvml-wrapper"] +x86-cpu = ["raw-cpuid"] +full = ["nvidia", "x86-cpu"] +``` + +### Dependency Rationale + +| Crate | Purpose | Why Not Shell Out? | +|-------|---------|-------------------| +| `nvml-wrapper` | NVIDIA GPU detection | Direct API access, no parsing, handles errors properly | +| `raw-cpuid` | x86 CPU cache/features | Direct CPU instruction, no external dependencies | +| `sysinfo` | Cross-platform fallback | Already in use, pure Rust | + +### References + +- [nvml-wrapper crate](https://crates.io/crates/nvml-wrapper) - [NVML API Docs](https://docs.nvidia.com/deploy/nvml-api/) +- [raw-cpuid crate](https://crates.io/crates/raw-cpuid) - [Intel CPUID](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) +- [sysinfo crate](https://crates.io/crates/sysinfo) + +--- + +## Implementation Order + +| Step | Task | Priority | Est. Effort | Files Changed | +|------|------|----------|-------------|---------------| +| 1 | Create `StorageType` enum and update `StorageDevice` | Critical | Low | `entities.rs` | +| 2 | Implement sysfs storage detection for Linux | Critical | Medium | `linux.rs`, `storage.rs` | +| 3 | Update `CpuInfo` with frequency/cache fields | Critical | Low | `entities.rs` | +| 4 | Implement sysfs CPU freq/cache detection | Critical | Medium | `linux.rs`, `cpu.rs` | +| 5 | Update `GpuDevice` with numeric memory fields | Critical | Low | `entities.rs` | +| 6 | Implement multi-method GPU detection | Critical | High | `linux.rs`, `gpu.rs` | +| 7 | Add NVML integration (feature-gated) | Critical | Medium | `linux.rs`, `Cargo.toml` | +| 8 | Update `MemoryModule` with part_number | Medium | Low | `entities.rs`, `memory.rs` | +| 9 | Update `NetworkInterface` with driver info | Medium | Medium | `entities.rs`, `linux.rs`, `network.rs` | +| 10 | Add rustdoc to all public items | High | Medium | All files | +| 11 | Add/update tests for ARM and x86 | High | High | `tests/` | +| 12 | Update examples | Medium | Low | `examples/` | + +--- + +## Related Documents + +- [GPU_DETECTION.md](./GPU_DETECTION.md) - Multi-method GPU detection strategy +- [STORAGE_DETECTION.md](./STORAGE_DETECTION.md) - Storage detection with ARM focus +- [CPU_ENHANCEMENTS.md](./CPU_ENHANCEMENTS.md) - CPU frequency and cache detection +- [MEMORY_ENHANCEMENTS.md](./MEMORY_ENHANCEMENTS.md) - Memory module enhancements +- [NETWORK_ENHANCEMENTS.md](./NETWORK_ENHANCEMENTS.md) - Network interface enhancements +- [RUSTDOC_STANDARDS.md](./RUSTDOC_STANDARDS.md) - Documentation standards +- [TESTING_STRATEGY.md](./TESTING_STRATEGY.md) - Testing approach for ARM and x86 + +--- + +## Changelog + +| Date | Version | Changes | +|------|---------|---------| +| 2024-12-29 | 0.2.0-plan | Initial enhancement plan | diff --git a/docs/GPU_DETECTION.md b/docs/GPU_DETECTION.md new file mode 100644 index 0000000..9b7bc29 --- /dev/null +++ b/docs/GPU_DETECTION.md @@ -0,0 +1,955 @@ +# GPU Detection Enhancement Plan + +> **Category:** Critical Issue +> **Target Platforms:** Linux (x86_64, aarch64) +> **Related Files:** `src/domain/entities.rs`, `src/adapters/secondary/system/linux.rs`, `src/domain/parsers/gpu.rs` (new) + +## Table of Contents + +1. [Problem Statement](#problem-statement) +2. [Current Implementation](#current-implementation) +3. [Multi-Method Detection Strategy](#multi-method-detection-strategy) +4. [Entity Changes](#entity-changes) +5. [Detection Method Details](#detection-method-details) +6. [Adapter Implementation](#adapter-implementation) +7. [Parser Implementation](#parser-implementation) +8. [Error Handling](#error-handling) +9. [Testing Requirements](#testing-requirements) +10. [References](#references) + +--- + +## Problem Statement + +### Current Issue + +The current GPU memory field returns a formatted string that consumers cannot reliably parse: + +```rust +// Current output +GpuDevice { + memory: "80 GB", // String - cannot parse reliably + // ... +} + +// Consumer code that fails: +let memory_mb = gpu.memory.parse::().unwrap_or(0.0) as u32 * 1024; +// Result: memory_mb = 0 (parse fails on "80 GB") +``` + +### Impact + +- CMDB inventory shows 0MB VRAM for all GPUs +- `metal-agent` must fall back to shelling out to `nvidia-smi` +- No support for AMD or Intel GPUs +- Detection fails silently + +### Requirements + +1. Return numeric memory values in MB (u64) +2. Detect GPUs using multiple methods with fallback chain +3. Support NVIDIA, AMD, and Intel GPUs +4. Work on both x86_64 and aarch64 architectures +5. Provide driver version information +6. Include detection method in output for debugging + +--- + +## Current Implementation + +### Location + +- **Entity:** `src/domain/entities.rs:235-251` +- **Adapter:** `src/adapters/secondary/system/linux.rs:172-231` + +### Current Detection Flow + +``` +┌─────────────────────────────────────────┐ +│ LinuxSystemInfoProvider::get_gpu_info() │ +└─────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ Try nvidia-smi │ + │ --query-gpu=... │ + └──────────────────┘ + │ + success? │ + ┌──────────┴──────────┐ + │ YES │ NO + ▼ ▼ + Parse CSV output ┌──────────────────┐ + Return devices │ Try lspci -nn │ + └──────────────────┘ + │ + Parse VGA/3D lines + Return basic info +``` + +### Current Limitations + +| Limitation | Impact | +|------------|--------| +| Only two detection methods | Misses AMD ROCm, Intel GPUs | +| String memory format | Breaks consumer parsing | +| No driver version | Missing CMDB field | +| No PCI bus ID | Can't correlate with NUMA | +| nvidia-smi parsing fragile | Format changes break detection | + +--- + +## Multi-Method Detection Strategy + +### Detection Priority Chain + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ GPU DETECTION CHAIN │ +│ │ +│ Priority 1: NVML (nvml-wrapper crate) │ +│ ├── Most accurate for NVIDIA GPUs │ +│ ├── Direct API access, no parsing │ +│ ├── Memory in bytes, convert to MB │ +│ └── Feature-gated: #[cfg(feature = "nvidia")] │ +│ │ │ +│ ▼ (if unavailable or no NVIDIA GPUs) │ +│ Priority 2: nvidia-smi command │ +│ ├── Fallback for NVIDIA when NVML unavailable │ +│ ├── Common on systems without development headers │ +│ └── Parse --query-gpu output with nounits flag │ +│ │ │ +│ ▼ (if unavailable or no NVIDIA GPUs) │ +│ Priority 3: ROCm SMI (rocm-smi) │ +│ ├── AMD GPU detection │ +│ ├── Parse JSON output when available │ +│ └── Common on AMD GPU systems │ +│ │ │ +│ ▼ (if unavailable or no AMD GPUs) │ +│ Priority 4: sysfs /sys/class/drm │ +│ ├── Linux DRM subsystem │ +│ ├── Works for all GPU vendors │ +│ ├── Memory info from /sys/class/drm/card*/device/mem_info_* │ +│ └── Vendor from /sys/class/drm/card*/device/vendor │ +│ │ │ +│ ▼ (if no GPUs found) │ +│ Priority 5: lspci with PCI ID database │ +│ ├── Enumerate all VGA/3D controllers │ +│ ├── Look up vendor:device in PCI ID database │ +│ └── No memory info available │ +│ │ │ +│ ▼ (if lspci unavailable) │ +│ Priority 6: sysinfo crate │ +│ ├── Cross-platform fallback │ +│ └── Limited GPU information │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### Method Capabilities Matrix + +| Method | NVIDIA | AMD | Intel | Memory | Driver | PCI Bus | NUMA | +|--------|--------|-----|-------|--------|--------|---------|------| +| NVML | Yes | No | No | Exact | Yes | Yes | Yes | +| nvidia-smi | Yes | No | No | Exact | Yes | Yes | No | +| rocm-smi | No | Yes | No | Exact | Yes | Yes | No | +| sysfs DRM | Yes | Yes | Yes | Varies | No | Yes | Yes | +| lspci | Yes | Yes | Yes | No | No | Yes | No | +| sysinfo | Limited | Limited | Limited | No | No | No | No | + +--- + +## Entity Changes + +### New GpuDevice Structure + +```rust +// src/domain/entities.rs + +/// GPU vendor classification +/// +/// # References +/// +/// - [PCI Vendor IDs](https://pci-ids.ucw.cz/) +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub enum GpuVendor { + /// NVIDIA Corporation (PCI vendor 0x10de) + Nvidia, + /// Advanced Micro Devices (PCI vendor 0x1002) + Amd, + /// Intel Corporation (PCI vendor 0x8086) + Intel, + /// Apple Inc. (integrated GPUs) + Apple, + /// Unknown or unrecognized vendor + Unknown, +} + +impl GpuVendor { + /// Convert PCI vendor ID to GpuVendor + /// + /// # Arguments + /// + /// * `vendor_id` - PCI vendor ID as hexadecimal string (e.g., "10de") + /// + /// # Example + /// + /// ``` + /// use hardware_report::GpuVendor; + /// + /// assert_eq!(GpuVendor::from_pci_vendor("10de"), GpuVendor::Nvidia); + /// assert_eq!(GpuVendor::from_pci_vendor("1002"), GpuVendor::Amd); + /// ``` + pub fn from_pci_vendor(vendor_id: &str) -> Self { + match vendor_id.to_lowercase().as_str() { + "10de" => GpuVendor::Nvidia, + "1002" => GpuVendor::Amd, + "8086" => GpuVendor::Intel, + _ => GpuVendor::Unknown, + } + } +} + +/// GPU device information +/// +/// Represents a discrete or integrated GPU detected in the system. +/// Memory values are provided in megabytes as unsigned integers for +/// reliable parsing by CMDB consumers. +/// +/// # Detection Methods +/// +/// GPUs are detected using multiple methods in priority order: +/// 1. **NVML** - NVIDIA Management Library (most accurate for NVIDIA) +/// 2. **nvidia-smi** - NVIDIA command-line tool (fallback) +/// 3. **rocm-smi** - AMD ROCm System Management Interface +/// 4. **sysfs** - Linux `/sys/class/drm` interface +/// 5. **lspci** - PCI device enumeration +/// 6. **sysinfo** - Cross-platform fallback +/// +/// # Memory Format +/// +/// Memory is always reported in **megabytes** as a `u64`. The previous +/// `memory: String` field (e.g., "80 GB") is deprecated. +/// +/// # Example +/// +/// ``` +/// use hardware_report::GpuDevice; +/// +/// // Calculate memory in GB from the numeric field +/// let memory_gb = gpu.memory_total_mb as f64 / 1024.0; +/// println!("GPU has {} GB memory", memory_gb); +/// ``` +/// +/// # References +/// +/// - [NVIDIA NVML API](https://docs.nvidia.com/deploy/nvml-api/) +/// - [AMD ROCm SMI](https://rocm.docs.amd.com/projects/rocm_smi_lib/en/latest/) +/// - [Linux DRM Subsystem](https://www.kernel.org/doc/html/latest/gpu/drm-uapi.html) +/// - [PCI ID Database](https://pci-ids.ucw.cz/) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GpuDevice { + /// GPU index (0-based, unique per system) + pub index: u32, + + /// GPU product name + /// + /// Examples: + /// - "NVIDIA H100 80GB HBM3" + /// - "AMD Instinct MI250X" + /// - "Intel Arc A770" + pub name: String, + + /// GPU UUID (globally unique identifier) + /// + /// Format varies by vendor: + /// - NVIDIA: "GPU-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + /// - AMD: May be empty or use different format + pub uuid: String, + + /// Total GPU memory in megabytes + /// + /// This is the primary memory field and should be used for all + /// programmatic access. Multiply by 1024 for KB, divide by 1024 + /// for GB. + /// + /// # Note + /// + /// Returns 0 if memory could not be determined (e.g., lspci-only detection). + pub memory_total_mb: u64, + + /// Available (free) GPU memory in megabytes + /// + /// This is a runtime value that reflects current memory usage. + /// Returns `None` if not queryable (requires NVML or ROCm). + pub memory_free_mb: Option, + + /// Used GPU memory in megabytes + /// + /// Calculated as `memory_total_mb - memory_free_mb` when available. + pub memory_used_mb: Option, + + /// PCI vendor:device ID (e.g., "10de:2330") + /// + /// Format: `{vendor_id}:{device_id}` in lowercase hexadecimal. + /// + /// # References + /// + /// - [PCI ID Database](https://pci-ids.ucw.cz/) + pub pci_id: String, + + /// PCI bus address (e.g., "0000:01:00.0") + /// + /// Format: `{domain}:{bus}:{device}.{function}` + /// + /// Useful for correlating with NUMA topology and other PCI devices. + pub pci_bus_id: Option, + + /// GPU vendor classification + pub vendor: GpuVendor, + + /// Vendor name as string (e.g., "NVIDIA", "AMD", "Intel") + /// + /// Provided for serialization compatibility. Use `vendor` field + /// for programmatic comparisons. + pub vendor_name: String, + + /// GPU driver version + /// + /// Examples: + /// - NVIDIA: "535.129.03" + /// - AMD: "6.3.6" + pub driver_version: Option, + + /// CUDA compute capability (NVIDIA only) + /// + /// Format: "major.minor" (e.g., "9.0" for Hopper, "8.9" for Ada) + /// + /// # References + /// + /// - [CUDA Compute Capability](https://developer.nvidia.com/cuda-gpus) + pub compute_capability: Option, + + /// GPU architecture name + /// + /// Examples: + /// - NVIDIA: "Hopper", "Ada Lovelace", "Ampere" + /// - AMD: "CDNA2", "RDNA3" + /// - Intel: "Xe-HPG" + pub architecture: Option, + + /// NUMA node affinity + /// + /// The NUMA node this GPU is attached to. Important for optimal + /// CPU-GPU memory transfers. + /// + /// Returns `None` on non-NUMA systems or if not determinable. + pub numa_node: Option, + + /// Power limit in watts (if available) + pub power_limit_watts: Option, + + /// Current temperature in Celsius (if available) + pub temperature_celsius: Option, + + /// Detection method that discovered this GPU + /// + /// One of: "nvml", "nvidia-smi", "rocm-smi", "sysfs", "lspci", "sysinfo" + /// + /// Useful for debugging and understanding data accuracy. + pub detection_method: String, +} + +impl Default for GpuDevice { + fn default() -> Self { + Self { + index: 0, + name: String::new(), + uuid: String::new(), + memory_total_mb: 0, + memory_free_mb: None, + memory_used_mb: None, + pci_id: String::new(), + pci_bus_id: None, + vendor: GpuVendor::Unknown, + vendor_name: "Unknown".to_string(), + driver_version: None, + compute_capability: None, + architecture: None, + numa_node: None, + power_limit_watts: None, + temperature_celsius: None, + detection_method: String::new(), + } + } +} +``` + +--- + +## Detection Method Details + +### Method 1: NVML (nvml-wrapper) + +**When:** Feature `nvidia` enabled, NVIDIA driver installed + +**Pros:** +- Most accurate data +- Direct API, no parsing +- Memory in bytes (exact) +- Full metadata + +**Cons:** +- Requires NVIDIA driver +- NVML library must be present +- NVIDIA GPUs only + +**sysfs paths used:** +- None (direct library calls) + +**References:** +- [nvml-wrapper crate](https://crates.io/crates/nvml-wrapper) +- [NVML API Reference](https://docs.nvidia.com/deploy/nvml-api/) +- [NVML Header](https://github.com/NVIDIA/nvidia-settings/blob/main/src/nvml.h) + +--- + +### Method 2: nvidia-smi + +**When:** NVML unavailable, `nvidia-smi` command available + +**Command:** +```bash +nvidia-smi --query-gpu=index,name,uuid,memory.total,memory.free,pci.bus_id,driver_version,compute_cap --format=csv,noheader,nounits +``` + +**Output format:** +``` +0, NVIDIA H100 80GB HBM3, GPU-xxxx, 81920, 81000, 00000000:01:00.0, 535.129.03, 9.0 +``` + +**Parsing notes:** +- Use `nounits` flag to get numeric values +- Memory is in MiB (mebibytes) +- Fields are comma-separated + +**References:** +- [nvidia-smi Documentation](https://developer.nvidia.com/nvidia-system-management-interface) + +--- + +### Method 3: ROCm SMI + +**When:** AMD GPU detected, `rocm-smi` command available + +**Command:** +```bash +rocm-smi --showproductname --showmeminfo vram --showdriver --json +``` + +**Output format (JSON):** +```json +{ + "card0": { + "Card series": "AMD Instinct MI250X", + "VRAM Total Memory (B)": "137438953472", + "Driver version": "6.3.6" + } +} +``` + +**References:** +- [ROCm SMI Documentation](https://rocm.docs.amd.com/projects/rocm_smi_lib/en/latest/) +- [ROCm GitHub](https://github.com/RadeonOpenCompute/rocm_smi_lib) + +--- + +### Method 4: sysfs DRM + +**When:** Linux, GPUs present in `/sys/class/drm` + +**sysfs paths:** +``` +/sys/class/drm/card{N}/device/ +├── vendor # PCI vendor ID (e.g., "0x10de") +├── device # PCI device ID (e.g., "0x2330") +├── subsystem_vendor # Subsystem vendor ID +├── subsystem_device # Subsystem device ID +├── numa_node # NUMA node affinity +├── mem_info_vram_total # AMD: VRAM total in bytes +├── mem_info_vram_used # AMD: VRAM used in bytes +└── driver/ # Symlink to driver + └── module/ + └── version # Driver version (some drivers) +``` + +**Vendor detection:** +- NVIDIA: vendor = 0x10de +- AMD: vendor = 0x1002 +- Intel: vendor = 0x8086 + +**Memory detection:** +- AMD: `/sys/class/drm/card*/device/mem_info_vram_total` +- Intel: `/sys/class/drm/card*/gt/addr_range` (varies) +- NVIDIA: Not available via sysfs (use NVML) + +**References:** +- [Linux DRM sysfs](https://www.kernel.org/doc/html/latest/gpu/drm-uapi.html) +- [sysfs ABI Documentation](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-drm) + +--- + +### Method 5: lspci + +**When:** Other methods unavailable or for initial enumeration + +**Command:** +```bash +lspci -nn -d ::0300 # VGA compatible controller +lspci -nn -d ::0302 # 3D controller (NVIDIA Tesla/compute) +``` + +**Output format:** +``` +01:00.0 3D controller [0302]: NVIDIA Corporation GH100 [H100 SXM5 80GB] [10de:2330] (rev a1) +``` + +**Parsing:** +- PCI bus ID: `01:00.0` +- Class: `3D controller [0302]` +- Vendor:Device: `[10de:2330]` +- Name: Everything between `:` and `[vendor:device]` + +**References:** +- [lspci man page](https://man7.org/linux/man-pages/man8/lspci.8.html) +- [PCI Class Codes](https://pci-ids.ucw.cz/read/PD/) + +--- + +### Method 6: sysinfo crate + +**When:** Last resort fallback + +**Usage:** +```rust +use sysinfo::System; + +let sys = System::new_all(); +// sysinfo doesn't currently expose GPU info +// but may in future versions +``` + +**Current limitation:** sysinfo does not expose GPU information as of v0.32. + +**References:** +- [sysinfo crate](https://crates.io/crates/sysinfo) + +--- + +## Adapter Implementation + +### File: `src/adapters/secondary/system/linux.rs` + +```rust +// Pseudocode for new implementation + +impl SystemInfoProvider for LinuxSystemInfoProvider { + async fn get_gpu_info(&self) -> Result { + let mut devices = Vec::new(); + + // Method 1: Try NVML (feature-gated) + #[cfg(feature = "nvidia")] + { + if let Ok(nvml_gpus) = self.detect_gpus_nvml().await { + devices.extend(nvml_gpus); + } + } + + // Method 2: Try nvidia-smi (if no NVML results) + if devices.is_empty() { + if let Ok(smi_gpus) = self.detect_gpus_nvidia_smi().await { + devices.extend(smi_gpus); + } + } + + // Method 3: Try rocm-smi for AMD + if let Ok(rocm_gpus) = self.detect_gpus_rocm_smi().await { + devices.extend(rocm_gpus); + } + + // Method 4: Try sysfs DRM + if let Ok(drm_gpus) = self.detect_gpus_sysfs_drm().await { + // Merge with existing or add new + self.merge_gpu_info(&mut devices, drm_gpus); + } + + // Method 5: Try lspci (for devices not yet found) + if let Ok(pci_gpus) = self.detect_gpus_lspci().await { + // Merge with existing or add new + self.merge_gpu_info(&mut devices, pci_gpus); + } + + // Enrich with NUMA info + self.enrich_gpu_numa_info(&mut devices).await; + + // Re-index + for (i, gpu) in devices.iter_mut().enumerate() { + gpu.index = i as u32; + } + + Ok(GpuInfo { devices }) + } +} +``` + +### Helper Methods + +```rust +impl LinuxSystemInfoProvider { + /// Detect GPUs using NVML library + /// + /// # Requirements + /// + /// - Feature `nvidia` must be enabled + /// - NVIDIA driver must be installed + /// - NVML library must be loadable + #[cfg(feature = "nvidia")] + async fn detect_gpus_nvml(&self) -> Result, SystemError> { + // Implementation using nvml-wrapper + todo!() + } + + /// Detect GPUs using nvidia-smi command + /// + /// # Requirements + /// + /// - `nvidia-smi` must be in PATH + /// - NVIDIA driver must be installed + async fn detect_gpus_nvidia_smi(&self) -> Result, SystemError> { + // Implementation using command execution + todo!() + } + + /// Detect AMD GPUs using rocm-smi command + /// + /// # Requirements + /// + /// - `rocm-smi` must be in PATH + /// - ROCm must be installed + async fn detect_gpus_rocm_smi(&self) -> Result, SystemError> { + // Implementation using command execution + todo!() + } + + /// Detect GPUs using sysfs DRM interface + /// + /// # References + /// + /// - [sysfs DRM ABI](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-drm) + async fn detect_gpus_sysfs_drm(&self) -> Result, SystemError> { + // Implementation reading /sys/class/drm + todo!() + } + + /// Detect GPUs using lspci command + /// + /// # Requirements + /// + /// - `lspci` must be in PATH (pciutils package) + async fn detect_gpus_lspci(&self) -> Result, SystemError> { + // Implementation using command execution + todo!() + } + + /// Merge GPU info from multiple sources + /// + /// GPUs are matched by PCI bus ID. Information from higher-priority + /// sources takes precedence, but missing fields are filled in from + /// lower-priority sources. + fn merge_gpu_info(&self, primary: &mut Vec, secondary: Vec) { + // Implementation + todo!() + } + + /// Enrich GPU devices with NUMA node information + /// + /// Reads NUMA affinity from `/sys/class/drm/card{N}/device/numa_node` + async fn enrich_gpu_numa_info(&self, devices: &mut [GpuDevice]) { + // Implementation + todo!() + } +} +``` + +--- + +## Parser Implementation + +### New File: `src/domain/parsers/gpu.rs` + +```rust +//! GPU information parsing functions +//! +//! This module provides pure parsing functions for GPU information from +//! various sources. All functions take string input and return parsed +//! results without performing I/O. +//! +//! # Supported Formats +//! +//! - nvidia-smi CSV output +//! - rocm-smi JSON output +//! - lspci text output +//! - sysfs file contents +//! +//! # Example +//! +//! ``` +//! use hardware_report::domain::parsers::gpu::parse_nvidia_smi_output; +//! +//! let output = "0, NVIDIA H100, GPU-xxx, 81920, 81000, 00:01:00.0, 535.129.03, 9.0"; +//! let gpus = parse_nvidia_smi_output(output).unwrap(); +//! assert_eq!(gpus[0].memory_total_mb, 81920); +//! ``` + +use crate::domain::{GpuDevice, GpuVendor}; + +/// Parse nvidia-smi CSV output into GPU devices +/// +/// # Arguments +/// +/// * `output` - Output from `nvidia-smi --query-gpu=... --format=csv,noheader,nounits` +/// +/// # Expected Format +/// +/// ```text +/// index, name, uuid, memory.total, memory.free, pci.bus_id, driver_version, compute_cap +/// 0, NVIDIA H100 80GB HBM3, GPU-xxxx, 81920, 81000, 00000000:01:00.0, 535.129.03, 9.0 +/// ``` +/// +/// # Errors +/// +/// Returns an error if the output format is invalid or cannot be parsed. +/// +/// # References +/// +/// - [nvidia-smi Query Options](https://developer.nvidia.com/nvidia-system-management-interface) +pub fn parse_nvidia_smi_output(output: &str) -> Result, String> { + todo!() +} + +/// Parse rocm-smi JSON output into GPU devices +/// +/// # Arguments +/// +/// * `output` - JSON output from `rocm-smi --json` +/// +/// # Expected Format +/// +/// ```json +/// { +/// "card0": { +/// "Card series": "AMD Instinct MI250X", +/// "VRAM Total Memory (B)": "137438953472", +/// "Driver version": "6.3.6" +/// } +/// } +/// ``` +/// +/// # Errors +/// +/// Returns an error if the JSON is invalid or required fields are missing. +/// +/// # References +/// +/// - [ROCm SMI Documentation](https://rocm.docs.amd.com/projects/rocm_smi_lib/en/latest/) +pub fn parse_rocm_smi_output(output: &str) -> Result, String> { + todo!() +} + +/// Parse lspci output for GPU devices +/// +/// # Arguments +/// +/// * `output` - Output from `lspci -nn` +/// +/// # Expected Format +/// +/// ```text +/// 01:00.0 3D controller [0302]: NVIDIA Corporation GH100 [H100 SXM5 80GB] [10de:2330] (rev a1) +/// ``` +/// +/// # Note +/// +/// This method cannot determine GPU memory. The `memory_total_mb` field +/// will be set to 0 for GPUs detected via lspci only. +/// +/// # References +/// +/// - [lspci man page](https://man7.org/linux/man-pages/man8/lspci.8.html) +pub fn parse_lspci_gpu_output(output: &str) -> Result, String> { + todo!() +} + +/// Parse PCI vendor ID to determine GPU vendor +/// +/// # Arguments +/// +/// * `vendor_id` - PCI vendor ID in hexadecimal (e.g., "10de", "0x10de") +/// +/// # Returns +/// +/// The corresponding `GpuVendor` enum value. +/// +/// # Example +/// +/// ``` +/// use hardware_report::domain::parsers::gpu::parse_pci_vendor; +/// use hardware_report::GpuVendor; +/// +/// assert_eq!(parse_pci_vendor("10de"), GpuVendor::Nvidia); +/// assert_eq!(parse_pci_vendor("0x1002"), GpuVendor::Amd); +/// ``` +/// +/// # References +/// +/// - [PCI Vendor IDs](https://pci-ids.ucw.cz/) +pub fn parse_pci_vendor(vendor_id: &str) -> GpuVendor { + todo!() +} + +/// Parse sysfs DRM memory info for AMD GPUs +/// +/// # Arguments +/// +/// * `content` - Content of `/sys/class/drm/card*/device/mem_info_vram_total` +/// +/// # Returns +/// +/// Memory size in megabytes. +/// +/// # References +/// +/// - [AMDGPU sysfs](https://www.kernel.org/doc/html/latest/gpu/amdgpu/driver-misc.html) +pub fn parse_sysfs_vram_total(content: &str) -> Result { + todo!() +} +``` + +--- + +## Error Handling + +### Error Types + +```rust +/// GPU detection-specific errors +#[derive(Debug, thiserror::Error)] +pub enum GpuDetectionError { + /// NVML library initialization failed + #[error("NVML initialization failed: {0}")] + NvmlInitFailed(String), + + /// No GPUs found by any method + #[error("No GPUs detected")] + NoGpusFound, + + /// Command execution failed + #[error("GPU detection command failed: {command}: {reason}")] + CommandFailed { + command: String, + reason: String, + }, + + /// Output parsing failed + #[error("Failed to parse GPU info from {source}: {reason}")] + ParseFailed { + source: String, + reason: String, + }, + + /// sysfs read failed + #[error("Failed to read sysfs path {path}: {reason}")] + SysfsFailed { + path: String, + reason: String, + }, +} +``` + +### Error Handling Strategy + +1. **Never fail completely** - Return partial results if some methods work +2. **Log warnings** - Log failures at detection methods for debugging +3. **Include detection_method** - So consumers know data accuracy +4. **Return empty GpuInfo** - If no GPUs found (not an error condition) + +--- + +## Testing Requirements + +### Unit Tests + +| Test | Description | +|------|-------------| +| `test_parse_nvidia_smi_output` | Parse valid nvidia-smi CSV | +| `test_parse_nvidia_smi_empty` | Handle empty nvidia-smi output | +| `test_parse_rocm_smi_output` | Parse valid rocm-smi JSON | +| `test_parse_lspci_output` | Parse lspci with multiple GPUs | +| `test_parse_pci_vendor` | Vendor ID to enum conversion | +| `test_gpu_merge` | Merging info from multiple sources | + +### Integration Tests + +| Test | Platform | Description | +|------|----------|-------------| +| `test_gpu_detection_nvidia` | x86_64 + NVIDIA | Full detection with real GPU | +| `test_gpu_detection_amd` | x86_64 + AMD | Full detection with AMD GPU | +| `test_gpu_detection_arm` | aarch64 | Detection on ARM (DGX Spark) | +| `test_gpu_detection_no_gpu` | Any | Graceful handling of no GPU | + +### Test Hardware Matrix + +| Platform | GPU | Test Target | +|----------|-----|-------------| +| x86_64 Linux | NVIDIA H100 | CI + Manual | +| x86_64 Linux | AMD MI250X | Manual | +| aarch64 Linux | NVIDIA (DGX Spark) | Manual | +| aarch64 Linux | No GPU | CI | + +--- + +## References + +### Official Documentation + +| Resource | URL | +|----------|-----| +| NVIDIA NVML API | https://docs.nvidia.com/deploy/nvml-api/ | +| NVIDIA SMI | https://developer.nvidia.com/nvidia-system-management-interface | +| AMD ROCm SMI | https://rocm.docs.amd.com/projects/rocm_smi_lib/en/latest/ | +| Linux DRM | https://www.kernel.org/doc/html/latest/gpu/drm-uapi.html | +| PCI ID Database | https://pci-ids.ucw.cz/ | +| CUDA Compute Capability | https://developer.nvidia.com/cuda-gpus | + +### Crate Documentation + +| Crate | URL | +|-------|-----| +| nvml-wrapper | https://docs.rs/nvml-wrapper | +| sysinfo | https://docs.rs/sysinfo | + +### Kernel Documentation + +| Path | Description | +|------|-------------| +| `/sys/class/drm/` | DRM subsystem sysfs | +| `/sys/class/drm/card*/device/vendor` | PCI vendor ID | +| `/sys/class/drm/card*/device/numa_node` | NUMA affinity | + +--- + +## Changelog + +| Date | Changes | +|------|---------| +| 2024-12-29 | Initial specification | diff --git a/docs/MEMORY_ENHANCEMENTS.md b/docs/MEMORY_ENHANCEMENTS.md new file mode 100644 index 0000000..a2e0396 --- /dev/null +++ b/docs/MEMORY_ENHANCEMENTS.md @@ -0,0 +1,662 @@ +# Memory Enhancement Plan + +> **Category:** Data Gap +> **Target Platforms:** Linux (x86_64, aarch64) +> **Priority:** Medium - Missing DIMM part_number and numeric fields + +## Table of Contents + +1. [Problem Statement](#problem-statement) +2. [Current Implementation](#current-implementation) +3. [Entity Changes](#entity-changes) +4. [Detection Method Details](#detection-method-details) +5. [Adapter Implementation](#adapter-implementation) +6. [Parser Implementation](#parser-implementation) +7. [Testing Requirements](#testing-requirements) +8. [References](#references) + +--- + +## Problem Statement + +### Current Issue + +The `MemoryModule` structure lacks the `part_number` field and uses string-based sizes: + +```rust +// Current struct - missing fields +pub struct MemoryModule { + pub size: String, // String, not numeric + pub type_: String, + pub speed: String, // String, not numeric + pub location: String, + pub manufacturer: String, + pub serial: String, + // Missing: part_number, rank, configured_voltage +} +``` + +### Impact + +- Cannot track memory part numbers for procurement/warranty +- Size as string breaks capacity calculations +- No memory rank information for performance analysis +- Missing voltage data for power analysis + +### Requirements + +1. **Add part_number field** - For asset tracking +2. **Numeric size field** - `size_bytes: u64` +3. **Numeric speed field** - `speed_mhz: Option` +4. **Additional metadata** - rank, voltage, bank locator + +--- + +## Current Implementation + +### Location + +- **Entity:** `src/domain/entities.rs:178-205` +- **Adapter:** `src/adapters/secondary/system/linux.rs:104-151` +- **Parser:** `src/domain/parsers/memory.rs` + +### Current Detection Flow + +``` +┌──────────────────────────────────────────┐ +│ LinuxSystemInfoProvider::get_memory_info()│ +└──────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ free -b │──────▶ Total memory + └──────────────────┘ + │ + ▼ + ┌───────────────────────┐ + │ dmidecode -t memory │──────▶ Module details + │ (requires privileges) │ + └───────────────────────┘ +``` + +--- + +## Entity Changes + +### New MemoryModule Structure + +```rust +// src/domain/entities.rs + +/// Memory technology type +/// +/// # References +/// +/// - [JEDEC Standards](https://www.jedec.org/) +/// - [SMBIOS Type 17](https://www.dmtf.org/standards/smbios) +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub enum MemoryType { + /// DDR4 SDRAM + Ddr4, + /// DDR5 SDRAM + Ddr5, + /// LPDDR4 (Low Power DDR4) + Lpddr4, + /// LPDDR5 (Low Power DDR5) + Lpddr5, + /// DDR3 SDRAM + Ddr3, + /// HBM (High Bandwidth Memory) + Hbm, + /// HBM2 + Hbm2, + /// HBM3 + Hbm3, + /// Unknown type + Unknown, +} + +impl MemoryType { + /// Parse memory type from SMBIOS/dmidecode string + /// + /// # Arguments + /// + /// * `type_str` - Type string from dmidecode (e.g., "DDR4", "DDR5") + /// + /// # Example + /// + /// ``` + /// use hardware_report::MemoryType; + /// + /// assert_eq!(MemoryType::from_string("DDR4"), MemoryType::Ddr4); + /// assert_eq!(MemoryType::from_string("LPDDR5"), MemoryType::Lpddr5); + /// ``` + pub fn from_string(type_str: &str) -> Self { + match type_str.to_uppercase().as_str() { + "DDR4" => MemoryType::Ddr4, + "DDR5" => MemoryType::Ddr5, + "LPDDR4" | "LPDDR4X" => MemoryType::Lpddr4, + "LPDDR5" | "LPDDR5X" => MemoryType::Lpddr5, + "DDR3" => MemoryType::Ddr3, + "HBM" => MemoryType::Hbm, + "HBM2" | "HBM2E" => MemoryType::Hbm2, + "HBM3" | "HBM3E" => MemoryType::Hbm3, + _ => MemoryType::Unknown, + } + } +} + +/// Memory form factor +/// +/// # References +/// +/// - [SMBIOS Type 17 Form Factor](https://www.dmtf.org/standards/smbios) +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub enum MemoryFormFactor { + /// Standard DIMM + Dimm, + /// Small Outline DIMM (laptops) + SoDimm, + /// Registered DIMM (servers) + Rdimm, + /// Load Reduced DIMM (servers) + Lrdimm, + /// Unbuffered DIMM + Udimm, + /// Non-volatile DIMM + Nvdimm, + /// High Bandwidth Memory + Hbm, + /// Unknown form factor + Unknown, +} + +/// Individual memory module (DIMM) information +/// +/// Represents a single memory module with comprehensive metadata +/// for CMDB inventory and capacity planning. +/// +/// # Detection Methods +/// +/// Memory module information is gathered from: +/// 1. **dmidecode -t memory** - SMBIOS Type 17 data (requires privileges) +/// 2. **sysfs /sys/devices/system/memory** - Basic memory info +/// 3. **sysinfo crate** - Total memory fallback +/// +/// # Part Number +/// +/// The `part_number` field contains the manufacturer's part number, +/// which is essential for: +/// - Procurement and ordering replacements +/// - Warranty tracking +/// - Compatibility verification +/// +/// # Example +/// +/// ``` +/// use hardware_report::MemoryModule; +/// +/// // Calculate total memory from modules +/// let total_gb: f64 = modules.iter() +/// .map(|m| m.size_bytes as f64) +/// .sum::() / (1024.0 * 1024.0 * 1024.0); +/// ``` +/// +/// # References +/// +/// - [JEDEC Memory Standards](https://www.jedec.org/) +/// - [SMBIOS Specification](https://www.dmtf.org/standards/smbios) +/// - [dmidecode](https://www.nongnu.org/dmidecode/) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MemoryModule { + /// Physical slot/bank locator (e.g., "DIMM_A1", "ChannelA-DIMM0") + /// + /// From SMBIOS "Locator" field. + pub location: String, + + /// Bank locator (e.g., "BANK 0", "P0 CHANNEL A") + /// + /// From SMBIOS "Bank Locator" field. + pub bank_locator: Option, + + /// Module size in bytes + /// + /// Primary size field for calculations. + pub size_bytes: u64, + + /// Module size as human-readable string (e.g., "32 GB") + /// + /// Convenience field for display. + pub size: String, + + /// Memory technology type + pub memory_type: MemoryType, + + /// Memory type as string (e.g., "DDR4", "DDR5") + /// + /// For backward compatibility. + pub type_: String, + + /// Memory speed in MT/s (megatransfers per second) + /// + /// This is the data rate, not the clock frequency. + /// DDR4-3200 = 3200 MT/s = 1600 MHz clock. + pub speed_mts: Option, + + /// Configured clock speed in MHz + pub speed_mhz: Option, + + /// Speed as string (e.g., "3200 MT/s") + /// + /// For backward compatibility. + pub speed: String, + + /// Form factor + pub form_factor: MemoryFormFactor, + + /// Manufacturer name (e.g., "Samsung", "Micron", "SK Hynix") + pub manufacturer: String, + + /// Module serial number + pub serial: String, + + /// Manufacturer part number + /// + /// Essential for procurement and warranty tracking. + /// + /// # Example + /// + /// - "M393A4K40EB3-CWE" (Samsung 32GB DDR4-3200) + /// - "MTA36ASF8G72PZ-3G2E1" (Micron 64GB DDR4-3200) + pub part_number: Option, + + /// Number of memory ranks + /// + /// Single rank (1R), Dual rank (2R), Quad rank (4R), Octal rank (8R). + /// Higher rank counts can affect performance and compatibility. + pub rank: Option, + + /// Data width in bits (e.g., 64, 72 for ECC) + pub data_width_bits: Option, + + /// Total width in bits (includes ECC bits if present) + pub total_width_bits: Option, + + /// Whether ECC (Error Correcting Code) is supported + pub ecc: Option, + + /// Configured voltage in volts (e.g., 1.2, 1.35) + pub voltage: Option, + + /// Minimum voltage in volts + pub voltage_min: Option, + + /// Maximum voltage in volts + pub voltage_max: Option, + + /// Asset tag (if set by administrator) + pub asset_tag: Option, +} + +impl Default for MemoryModule { + fn default() -> Self { + Self { + location: String::new(), + bank_locator: None, + size_bytes: 0, + size: String::new(), + memory_type: MemoryType::Unknown, + type_: String::new(), + speed_mts: None, + speed_mhz: None, + speed: String::new(), + form_factor: MemoryFormFactor::Unknown, + manufacturer: String::new(), + serial: String::new(), + part_number: None, + rank: None, + data_width_bits: None, + total_width_bits: None, + ecc: None, + voltage: None, + voltage_min: None, + voltage_max: None, + asset_tag: None, + } + } +} + +/// System memory information +/// +/// Container for overall memory statistics and individual modules. +/// +/// # References +/// +/// - [/proc/meminfo](https://man7.org/linux/man-pages/man5/proc.5.html) +/// - [SMBIOS Type 16 & 17](https://www.dmtf.org/standards/smbios) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MemoryInfo { + /// Total system memory in bytes + pub total_bytes: u64, + + /// Total memory as human-readable string (e.g., "256 GB") + pub total: String, + + /// Primary memory type across all modules + pub type_: String, + + /// Primary memory speed + pub speed: String, + + /// Number of populated DIMM slots + pub populated_slots: u32, + + /// Total number of DIMM slots + pub total_slots: Option, + + /// Maximum supported memory capacity in bytes + pub max_capacity_bytes: Option, + + /// Whether ECC is enabled system-wide + pub ecc_enabled: Option, + + /// Individual memory modules + pub modules: Vec, +} +``` + +--- + +## Detection Method Details + +### Method 1: dmidecode -t memory + +**When:** Linux with privileges (primary source) + +**Command:** +```bash +dmidecode -t 17 # Memory Device (each DIMM) +dmidecode -t 16 # Physical Memory Array (capacity/slots) +``` + +**SMBIOS Type 17 Fields:** + +| Field | Description | Maps To | +|-------|-------------|---------| +| Size | Module size | `size_bytes`, `size` | +| Locator | Slot name | `location` | +| Bank Locator | Bank name | `bank_locator` | +| Type | DDR4, DDR5, etc. | `type_`, `memory_type` | +| Speed | MT/s rating | `speed_mts`, `speed` | +| Configured Memory Speed | Actual MHz | `speed_mhz` | +| Manufacturer | OEM name | `manufacturer` | +| Serial Number | Serial | `serial` | +| Part Number | OEM part# | `part_number` | +| Rank | 1, 2, 4, 8 | `rank` | +| Configured Voltage | Volts | `voltage` | +| Form Factor | DIMM, SODIMM | `form_factor` | +| Data Width | 64, 72 bits | `data_width_bits` | +| Total Width | 64, 72 bits | `total_width_bits` | + +**Example dmidecode output:** +``` +Memory Device + Size: 32 GB + Locator: DIMM_A1 + Bank Locator: BANK 0 + Type: DDR4 + Speed: 3200 MT/s + Manufacturer: Samsung + Serial Number: 12345678 + Part Number: M393A4K40EB3-CWE + Rank: 2 + Configured Memory Speed: 3200 MT/s + Configured Voltage: 1.2 V +``` + +**References:** +- [dmidecode](https://www.nongnu.org/dmidecode/) +- [SMBIOS Specification](https://www.dmtf.org/standards/smbios) + +--- + +### Method 2: sysfs /sys/devices/system/memory + +**When:** Basic memory info without privileges + +**Paths:** +``` +/sys/devices/system/memory/ +├── block_size_bytes # Memory block size +├── memory0/ # First memory block +│ ├── online # 1 if online +│ ├── state # online/offline +│ └── phys_index # Physical address +└── ... +``` + +**Limitation:** Does not provide DIMM-level details like part number. + +--- + +### Method 3: /proc/meminfo + +**When:** Total memory fallback + +**Path:** `/proc/meminfo` + +**Format:** +``` +MemTotal: 263736560 kB +MemFree: 8472348 kB +MemAvailable: 245678912 kB +... +``` + +--- + +## Parser Implementation + +### File: `src/domain/parsers/memory.rs` + +```rust +//! Memory information parsing functions +//! +//! This module provides pure parsing functions for memory information from +//! various sources, primarily dmidecode output. +//! +//! # References +//! +//! - [SMBIOS Specification](https://www.dmtf.org/standards/smbios) +//! - [dmidecode](https://www.nongnu.org/dmidecode/) + +use crate::domain::{MemoryFormFactor, MemoryInfo, MemoryModule, MemoryType}; + +/// Parse dmidecode Type 17 (Memory Device) output +/// +/// # Arguments +/// +/// * `output` - Output from `dmidecode -t 17` +/// +/// # Returns +/// +/// Vector of memory modules with all available fields populated. +/// +/// # Example +/// +/// ``` +/// use hardware_report::domain::parsers::memory::parse_dmidecode_memory_device; +/// +/// let output = r#" +/// Memory Device +/// Size: 32 GB +/// Locator: DIMM_A1 +/// Part Number: M393A4K40EB3-CWE +/// "#; +/// +/// let modules = parse_dmidecode_memory_device(output).unwrap(); +/// assert_eq!(modules[0].part_number, Some("M393A4K40EB3-CWE".to_string())); +/// ``` +/// +/// # References +/// +/// - [SMBIOS Type 17](https://www.dmtf.org/standards/smbios) +pub fn parse_dmidecode_memory_device(output: &str) -> Result, String> { + todo!() +} + +/// Parse memory size string to bytes +/// +/// # Arguments +/// +/// * `size_str` - Size string (e.g., "32 GB", "16384 MB", "No Module Installed") +/// +/// # Returns +/// +/// Size in bytes, or 0 if not installed/unknown. +/// +/// # Example +/// +/// ``` +/// use hardware_report::domain::parsers::memory::parse_memory_size; +/// +/// assert_eq!(parse_memory_size("32 GB"), 32 * 1024 * 1024 * 1024); +/// assert_eq!(parse_memory_size("16384 MB"), 16384 * 1024 * 1024); +/// assert_eq!(parse_memory_size("No Module Installed"), 0); +/// ``` +pub fn parse_memory_size(size_str: &str) -> u64 { + let s = size_str.trim(); + + if s.contains("No Module") || s.contains("Unknown") || s.contains("Not Installed") { + return 0; + } + + let parts: Vec<&str> = s.split_whitespace().collect(); + if parts.len() < 2 { + return 0; + } + + let value: u64 = match parts[0].parse() { + Ok(v) => v, + Err(_) => return 0, + }; + + match parts[1].to_uppercase().as_str() { + "GB" => value * 1024 * 1024 * 1024, + "MB" => value * 1024 * 1024, + "KB" => value * 1024, + _ => 0, + } +} + +/// Parse memory speed to MT/s +/// +/// # Arguments +/// +/// * `speed_str` - Speed string (e.g., "3200 MT/s", "2666 MHz") +/// +/// # Returns +/// +/// Speed in MT/s. +pub fn parse_memory_speed(speed_str: &str) -> Option { + let s = speed_str.trim(); + + if s.contains("Unknown") { + return None; + } + + let parts: Vec<&str> = s.split_whitespace().collect(); + if parts.is_empty() { + return None; + } + + parts[0].parse().ok() +} + +/// Parse /proc/meminfo for total memory +/// +/// # Arguments +/// +/// * `content` - Content of /proc/meminfo +/// +/// # Returns +/// +/// Total memory in bytes. +/// +/// # References +/// +/// - [/proc/meminfo](https://man7.org/linux/man-pages/man5/proc.5.html) +pub fn parse_proc_meminfo(content: &str) -> Result { + for line in content.lines() { + if line.starts_with("MemTotal:") { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + if let Ok(kb) = parts[1].parse::() { + return Ok(kb * 1024); // Convert KB to bytes + } + } + } + } + Err("MemTotal not found in /proc/meminfo".to_string()) +} + +/// Parse form factor string to enum +/// +/// # Arguments +/// +/// * `ff_str` - Form factor from dmidecode (e.g., "DIMM", "SODIMM") +pub fn parse_form_factor(ff_str: &str) -> MemoryFormFactor { + match ff_str.trim().to_uppercase().as_str() { + "DIMM" => MemoryFormFactor::Dimm, + "SODIMM" | "SO-DIMM" => MemoryFormFactor::SoDimm, + "RDIMM" => MemoryFormFactor::Rdimm, + "LRDIMM" => MemoryFormFactor::Lrdimm, + "UDIMM" => MemoryFormFactor::Udimm, + "NVDIMM" => MemoryFormFactor::Nvdimm, + _ => MemoryFormFactor::Unknown, + } +} +``` + +--- + +## Testing Requirements + +### Unit Tests + +| Test | Description | +|------|-------------| +| `test_parse_dmidecode_memory` | Parse full dmidecode output | +| `test_parse_memory_size` | Size string parsing | +| `test_parse_memory_speed` | Speed string parsing | +| `test_parse_proc_meminfo` | /proc/meminfo parsing | +| `test_memory_type_from_string` | Type enum conversion | +| `test_form_factor_parsing` | Form factor parsing | + +### Integration Tests + +| Test | Platform | Description | +|------|----------|-------------| +| `test_memory_detection` | Linux | Full memory detection | +| `test_memory_without_sudo` | Linux | Fallback without privileges | + +--- + +## References + +### Official Documentation + +| Resource | URL | +|----------|-----| +| JEDEC Standards | https://www.jedec.org/ | +| SMBIOS Specification | https://www.dmtf.org/standards/smbios | +| dmidecode | https://www.nongnu.org/dmidecode/ | +| /proc/meminfo | https://man7.org/linux/man-pages/man5/proc.5.html | + +--- + +## Changelog + +| Date | Changes | +|------|---------| +| 2024-12-29 | Initial specification | diff --git a/docs/NETWORK_ENHANCEMENTS.md b/docs/NETWORK_ENHANCEMENTS.md new file mode 100644 index 0000000..12efe97 --- /dev/null +++ b/docs/NETWORK_ENHANCEMENTS.md @@ -0,0 +1,620 @@ +# Network Interface Enhancement Plan + +> **Category:** Data Gap +> **Target Platforms:** Linux (x86_64, aarch64) +> **Priority:** Medium - Missing driver version and operational state + +## Table of Contents + +1. [Problem Statement](#problem-statement) +2. [Current Implementation](#current-implementation) +3. [Entity Changes](#entity-changes) +4. [Detection Method Details](#detection-method-details) +5. [Adapter Implementation](#adapter-implementation) +6. [Parser Implementation](#parser-implementation) +7. [Testing Requirements](#testing-requirements) +8. [References](#references) + +--- + +## Problem Statement + +### Current Issue + +The `NetworkInterface` structure lacks driver information and operational state: + +```rust +// Current struct - missing fields +pub struct NetworkInterface { + pub name: String, + pub mac: String, + pub ip: String, + pub prefix: String, + pub speed: Option, // String, not numeric + pub type_: String, + pub vendor: String, + pub model: String, + pub pci_id: String, + pub numa_node: Option, + // Missing: driver, driver_version, mtu, is_up, is_virtual +} +``` + +### Impact + +- Cannot track NIC driver versions for compatibility +- No MTU information for network configuration validation +- Cannot determine interface operational state +- Cannot distinguish physical vs virtual interfaces + +### Requirements + +1. **Driver information** - driver name and version +2. **Numeric speed** - `speed_mbps: Option` +3. **Operational state** - `is_up: bool` +4. **MTU** - `mtu: u32` +5. **Virtual interface detection** - `is_virtual: bool` + +--- + +## Current Implementation + +### Location + +- **Entity:** `src/domain/entities.rs:263-285` +- **Adapter:** `src/adapters/secondary/system/linux.rs:234-252` +- **Parser:** `src/domain/parsers/network.rs` + +### Current Detection + +``` +┌────────────────────────────────────────────┐ +│ LinuxSystemInfoProvider::get_network_info()│ +└────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ ip addr show │ + └──────────────────┘ + │ + ▼ + Parse interface list +``` + +--- + +## Entity Changes + +### New NetworkInterface Structure + +```rust +// src/domain/entities.rs + +/// Network interface type classification +/// +/// # References +/// +/// - [Linux Networking](https://www.kernel.org/doc/html/latest/networking/index.html) +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub enum NetworkInterfaceType { + /// Physical Ethernet interface + Ethernet, + /// Wireless interface + Wireless, + /// Loopback interface + Loopback, + /// Bridge interface + Bridge, + /// VLAN interface + Vlan, + /// Bond/LAG interface + Bond, + /// Virtual Ethernet (veth pair) + Veth, + /// TUN/TAP interface + TunTap, + /// InfiniBand interface + Infiniband, + /// Macvlan interface + Macvlan, + /// Unknown type + Unknown, +} + +/// Network interface information +/// +/// Represents a network interface with comprehensive metadata for +/// CMDB inventory and network configuration. +/// +/// # Detection Methods +/// +/// Network information is gathered from multiple sources: +/// 1. **sysfs /sys/class/net** - Primary source for most fields +/// 2. **ip command** - Address and routing information +/// 3. **ethtool** - Speed, driver, firmware (requires privileges) +/// +/// # Driver Information +/// +/// The `driver` and `driver_version` fields are essential for: +/// - Compatibility tracking +/// - Firmware update planning +/// - Troubleshooting network issues +/// +/// # Example +/// +/// ``` +/// use hardware_report::NetworkInterface; +/// +/// // Check if interface is usable +/// if iface.is_up && !iface.is_virtual && iface.speed_mbps.unwrap_or(0) >= 10000 { +/// println!("{} is a 10G+ physical interface", iface.name); +/// } +/// ``` +/// +/// # References +/// +/// - [Linux sysfs net](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net) +/// - [ethtool](https://man7.org/linux/man-pages/man8/ethtool.8.html) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct NetworkInterface { + /// Interface name (e.g., "eth0", "ens192", "enp0s3") + pub name: String, + + /// MAC address in colon-separated format + /// + /// Example: "00:11:22:33:44:55" + pub mac: String, + + /// Permanent MAC address (if different from current) + /// + /// Some NICs allow changing the MAC address. + pub permanent_mac: Option, + + /// Primary IPv4 address + pub ip: String, + + /// IPv4 addresses with prefix length + pub ipv4_addresses: Vec, + + /// IPv6 addresses with prefix length + pub ipv6_addresses: Vec, + + /// Network prefix length (e.g., "24" for /24) + pub prefix: String, + + /// Link speed in Mbps + /// + /// Common values: 1000 (1G), 10000 (10G), 25000 (25G), 100000 (100G) + pub speed_mbps: Option, + + /// Link speed as string (e.g., "10000 Mbps") + pub speed: Option, + + /// Interface type classification + pub interface_type: NetworkInterfaceType, + + /// Interface type as string (for backward compatibility) + pub type_: String, + + /// Hardware vendor name + pub vendor: String, + + /// Hardware model/description + pub model: String, + + /// PCI vendor:device ID (e.g., "8086:1521") + pub pci_id: String, + + /// PCI bus address (e.g., "0000:01:00.0") + pub pci_bus_id: Option, + + /// NUMA node affinity + pub numa_node: Option, + + /// Kernel driver in use + /// + /// Examples: "igb", "i40e", "mlx5_core", "bnxt_en" + pub driver: Option, + + /// Driver version + /// + /// From `/sys/module/{driver}/version` or ethtool. + pub driver_version: Option, + + /// Firmware version + /// + /// From ethtool -i. + pub firmware_version: Option, + + /// Maximum Transmission Unit in bytes + /// + /// Standard: 1500, Jumbo frames: 9000 + pub mtu: u32, + + /// Whether the interface is operationally up + /// + /// From `/sys/class/net/{iface}/operstate`. + pub is_up: bool, + + /// Whether this is a virtual interface + /// + /// Virtual interfaces include: bridges, VLANs, bonds, veths, tun/tap. + pub is_virtual: bool, + + /// Whether this interface is a loopback + pub is_loopback: bool, + + /// Link detected (carrier present) + pub carrier: Option, + + /// Duplex mode: "full", "half", or None if not applicable + pub duplex: Option, + + /// Auto-negotiation status + pub autoneg: Option, + + /// Wake-on-LAN support + pub wake_on_lan: Option, + + /// Transmit queue length + pub tx_queue_len: Option, + + /// Number of RX queues + pub rx_queues: Option, + + /// Number of TX queues + pub tx_queues: Option, + + /// SR-IOV Virtual Functions enabled + pub sriov_numvfs: Option, + + /// Maximum SR-IOV Virtual Functions + pub sriov_totalvfs: Option, +} + +impl Default for NetworkInterface { + fn default() -> Self { + Self { + name: String::new(), + mac: String::new(), + permanent_mac: None, + ip: String::new(), + ipv4_addresses: Vec::new(), + ipv6_addresses: Vec::new(), + prefix: String::new(), + speed_mbps: None, + speed: None, + interface_type: NetworkInterfaceType::Unknown, + type_: String::new(), + vendor: String::new(), + model: String::new(), + pci_id: String::new(), + pci_bus_id: None, + numa_node: None, + driver: None, + driver_version: None, + firmware_version: None, + mtu: 1500, + is_up: false, + is_virtual: false, + is_loopback: false, + carrier: None, + duplex: None, + autoneg: None, + wake_on_lan: None, + tx_queue_len: None, + rx_queues: None, + tx_queues: None, + sriov_numvfs: None, + sriov_totalvfs: None, + } + } +} +``` + +--- + +## Detection Method Details + +### Method 1: sysfs /sys/class/net (Primary) + +**sysfs paths:** + +``` +/sys/class/net/{iface}/ +├── address # MAC address +├── addr_len # Address length +├── mtu # MTU +├── operstate # up/down/unknown +├── carrier # 1=link, 0=no link +├── speed # Speed in Mbps (may be -1) +├── duplex # full/half +├── tx_queue_len # TX queue length +├── type # Interface type (ARPHRD_*) +├── device/ # -> PCI device (if physical) +│ ├── vendor # PCI vendor ID +│ ├── device # PCI device ID +│ ├── numa_node # NUMA affinity +│ ├── driver/ # -> driver symlink +│ │ └── module/ +│ │ └── version # Driver version +│ └── net_dev/queues/ +│ ├── rx-*/ # RX queues +│ └── tx-*/ # TX queues +├── queues/ +│ ├── rx-*/ # RX queues +│ └── tx-*/ # TX queues +└── statistics/ # Interface statistics + ├── rx_bytes + ├── tx_bytes + ├── rx_packets + └── tx_packets +``` + +**Virtual interface detection:** +```rust +fn is_virtual_interface(name: &str, sysfs_path: &Path) -> bool { + // Virtual interfaces don't have a /device symlink + !sysfs_path.join("device").exists() + || name.starts_with("veth") + || name.starts_with("br") + || name.starts_with("virbr") + || name.starts_with("docker") + || name.starts_with("vlan") + || name.contains("bond") + || name.starts_with("tun") + || name.starts_with("tap") +} +``` + +**References:** +- [sysfs-class-net](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net) + +--- + +### Method 2: ethtool + +**When:** For driver/firmware info, detailed link settings + +**Commands:** +```bash +# Driver info +ethtool -i eth0 + +# Link settings +ethtool eth0 + +# Firmware/EEPROM info +ethtool -e eth0 +``` + +**ethtool -i output:** +``` +driver: igb +version: 5.4.0-k +firmware-version: 1.67, 0x80000d38 +bus-info: 0000:01:00.0 +``` + +**References:** +- [ethtool man page](https://man7.org/linux/man-pages/man8/ethtool.8.html) + +--- + +### Method 3: ip command + +**When:** For IP addresses + +**Command:** +```bash +ip -j addr show # JSON output +``` + +**References:** +- [ip command](https://man7.org/linux/man-pages/man8/ip.8.html) + +--- + +## Parser Implementation + +### File: `src/domain/parsers/network.rs` + +```rust +//! Network interface parsing functions +//! +//! # References +//! +//! - [sysfs-class-net](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net) +//! - [ethtool](https://man7.org/linux/man-pages/man8/ethtool.8.html) + +use crate::domain::{NetworkInterface, NetworkInterfaceType}; + +/// Parse sysfs operstate to boolean +/// +/// # Arguments +/// +/// * `content` - Content of `/sys/class/net/{iface}/operstate` +/// +/// # Returns +/// +/// `true` if interface is up. +/// +/// # Example +/// +/// ``` +/// use hardware_report::domain::parsers::network::parse_operstate; +/// +/// assert!(parse_operstate("up")); +/// assert!(!parse_operstate("down")); +/// ``` +pub fn parse_operstate(content: &str) -> bool { + content.trim().to_lowercase() == "up" +} + +/// Parse sysfs speed to Mbps +/// +/// # Arguments +/// +/// * `content` - Content of `/sys/class/net/{iface}/speed` +/// +/// # Returns +/// +/// Speed in Mbps, or None if invalid/unknown. +pub fn parse_sysfs_speed(content: &str) -> Option { + let speed: i32 = content.trim().parse().ok()?; + if speed > 0 { + Some(speed as u32) + } else { + None // -1 means unknown + } +} + +/// Parse ethtool -i output for driver info +/// +/// # Arguments +/// +/// * `output` - Output from `ethtool -i {iface}` +/// +/// # Returns +/// +/// Tuple of (driver, version, firmware_version, bus_info). +/// +/// # References +/// +/// - [ethtool](https://man7.org/linux/man-pages/man8/ethtool.8.html) +pub fn parse_ethtool_driver_info(output: &str) -> (Option, Option, Option, Option) { + let mut driver = None; + let mut version = None; + let mut firmware = None; + let mut bus_info = None; + + for line in output.lines() { + let parts: Vec<&str> = line.splitn(2, ':').collect(); + if parts.len() != 2 { + continue; + } + + let key = parts[0].trim(); + let value = parts[1].trim(); + + match key { + "driver" => driver = Some(value.to_string()), + "version" => version = Some(value.to_string()), + "firmware-version" => firmware = Some(value.to_string()), + "bus-info" => bus_info = Some(value.to_string()), + _ => {} + } + } + + (driver, version, firmware, bus_info) +} + +/// Parse ip -j addr output +/// +/// # Arguments +/// +/// * `output` - JSON output from `ip -j addr show` +/// +/// # References +/// +/// - [ip-address](https://man7.org/linux/man-pages/man8/ip-address.8.html) +pub fn parse_ip_json(output: &str) -> Result, String> { + todo!() +} + +/// Determine interface type from name and sysfs +/// +/// # Arguments +/// +/// * `name` - Interface name +/// * `sysfs_type` - Content of `/sys/class/net/{name}/type` +pub fn determine_interface_type(name: &str, sysfs_type: Option<&str>) -> NetworkInterfaceType { + // Check name patterns first + if name == "lo" { + return NetworkInterfaceType::Loopback; + } + if name.starts_with("br") || name.starts_with("virbr") { + return NetworkInterfaceType::Bridge; + } + if name.starts_with("bond") { + return NetworkInterfaceType::Bond; + } + if name.starts_with("veth") { + return NetworkInterfaceType::Veth; + } + if name.contains(".") || name.starts_with("vlan") { + return NetworkInterfaceType::Vlan; + } + if name.starts_with("tun") || name.starts_with("tap") { + return NetworkInterfaceType::TunTap; + } + if name.starts_with("ib") { + return NetworkInterfaceType::Infiniband; + } + if name.starts_with("wl") || name.starts_with("wlan") { + return NetworkInterfaceType::Wireless; + } + + // Check sysfs type (ARPHRD_* values) + if let Some(type_str) = sysfs_type { + if let Ok(type_num) = type_str.trim().parse::() { + match type_num { + 1 => return NetworkInterfaceType::Ethernet, // ARPHRD_ETHER + 772 => return NetworkInterfaceType::Loopback, // ARPHRD_LOOPBACK + 32 => return NetworkInterfaceType::Infiniband, // ARPHRD_INFINIBAND + _ => {} + } + } + } + + // Default to Ethernet for physical interfaces + if name.starts_with("eth") || name.starts_with("en") { + NetworkInterfaceType::Ethernet + } else { + NetworkInterfaceType::Unknown + } +} +``` + +--- + +## Testing Requirements + +### Unit Tests + +| Test | Description | +|------|-------------| +| `test_parse_operstate` | Parse up/down states | +| `test_parse_sysfs_speed` | Parse speed values | +| `test_parse_ethtool_driver` | Parse ethtool -i output | +| `test_interface_type_detection` | Name to type mapping | +| `test_virtual_interface_detection` | Virtual interface check | + +### Integration Tests + +| Test | Platform | Description | +|------|----------|-------------| +| `test_network_detection` | Linux | Full network detection | +| `test_sysfs_network` | Linux | sysfs parsing | +| `test_ethtool_info` | Linux | ethtool integration | + +--- + +## References + +### Official Documentation + +| Resource | URL | +|----------|-----| +| sysfs-class-net | https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net | +| ethtool | https://man7.org/linux/man-pages/man8/ethtool.8.html | +| ip command | https://man7.org/linux/man-pages/man8/ip.8.html | +| Linux ARPHRD | https://github.com/torvalds/linux/blob/master/include/uapi/linux/if_arp.h | + +--- + +## Changelog + +| Date | Changes | +|------|---------| +| 2024-12-29 | Initial specification | diff --git a/docs/RUSTDOC_STANDARDS.md b/docs/RUSTDOC_STANDARDS.md new file mode 100644 index 0000000..b485fa5 --- /dev/null +++ b/docs/RUSTDOC_STANDARDS.md @@ -0,0 +1,560 @@ +# Rustdoc Standards and Guidelines + +> **Purpose:** Define documentation standards for the `hardware_report` crate +> **Audience:** Contributors and maintainers + +## Table of Contents + +1. [Overview](#overview) +2. [Documentation Requirements](#documentation-requirements) +3. [Rustdoc Format](#rustdoc-format) +4. [External References](#external-references) +5. [Examples](#examples) +6. [Module Documentation](#module-documentation) +7. [Linting and CI](#linting-and-ci) + +--- + +## Overview + +All public APIs in `hardware_report` must be documented with rustdoc comments. This ensures: + +1. **Discoverability** - Engineers can find what they need +2. **Correctness** - Examples are tested via `cargo test --doc` +3. **Traceability** - Links to official specifications and kernel docs +4. **Maintainability** - Clear contracts for each component + +### Guiding Principles + +- **Every public item gets a doc comment** - structs, enums, functions, traits, modules +- **Link to official references** - kernel docs, hardware specs, crate docs +- **Include examples** - runnable code in doc comments +- **Explain "why" not just "what"** - context for design decisions + +--- + +## Documentation Requirements + +### Required for ALL Public Items + +| Item Type | Required Sections | +|-----------|-------------------| +| Module | Purpose, contents overview | +| Struct | Description, fields, example usage | +| Enum | Description, variants, when to use each | +| Function | Purpose, arguments, returns, errors, example | +| Trait | Purpose, implementors, example | +| Constant | Purpose, value explanation | + +### Required External Links + +When documenting hardware-related items, include links to: + +| Topic | Link To | +|-------|---------| +| sysfs paths | Kernel documentation | +| PCI IDs | pci-ids.ucw.cz | +| SMBIOS fields | DMTF SMBIOS spec | +| NVMe | nvmexpress.org | +| GPU (NVIDIA) | NVIDIA developer docs | +| GPU (AMD) | ROCm documentation | +| Memory specs | JEDEC | +| CPU (x86) | Intel/AMD SDM | +| CPU (ARM) | ARM developer documentation | + +--- + +## Rustdoc Format + +### Basic Structure + +```rust +/// Short one-line description. +/// +/// Longer description that explains the purpose, context, and usage +/// of this item. Can span multiple paragraphs. +/// +/// # Arguments +/// +/// * `param1` - Description of first parameter +/// * `param2` - Description of second parameter +/// +/// # Returns +/// +/// Description of return value. +/// +/// # Errors +/// +/// Description of error conditions. +/// +/// # Panics +/// +/// Conditions under which this function panics (if any). +/// +/// # Safety +/// +/// For unsafe functions, explain the invariants. +/// +/// # Example +/// +/// ```rust +/// use hardware_report::SomeItem; +/// +/// let result = some_function(arg1, arg2); +/// assert!(result.is_ok()); +/// ``` +/// +/// # References +/// +/// - [Link Text](https://url) +/// - [Another Reference](https://url) +pub fn some_function(param1: Type1, param2: Type2) -> Result { + // ... +} +``` + +### Struct Documentation + +```rust +/// GPU device information. +/// +/// Represents a discrete or integrated GPU detected in the system. +/// Memory values are provided in megabytes as unsigned integers for +/// reliable parsing by CMDB consumers. +/// +/// # Detection Methods +/// +/// GPUs are detected using multiple methods in priority order: +/// 1. **NVML** - NVIDIA Management Library (most accurate) +/// 2. **nvidia-smi** - NVIDIA CLI tool +/// 3. **rocm-smi** - AMD GPU tool +/// 4. **sysfs** - Linux `/sys/class/drm` +/// 5. **lspci** - PCI enumeration +/// +/// # Memory Format +/// +/// Memory is reported in **megabytes** as `u64`. The previous string +/// format (e.g., "80 GB") is deprecated. +/// +/// # Example +/// +/// ```rust +/// use hardware_report::GpuDevice; +/// +/// fn process_gpu(gpu: &GpuDevice) { +/// // Calculate memory in GB +/// let memory_gb = gpu.memory_total_mb as f64 / 1024.0; +/// println!("{}: {} GB", gpu.name, memory_gb); +/// +/// // Check vendor +/// if gpu.vendor == GpuVendor::Nvidia { +/// println!("CUDA Compute: {:?}", gpu.compute_capability); +/// } +/// } +/// ``` +/// +/// # References +/// +/// - [NVIDIA NVML API](https://docs.nvidia.com/deploy/nvml-api/) +/// - [AMD ROCm SMI](https://rocm.docs.amd.com/projects/rocm_smi_lib/en/latest/) +/// - [Linux DRM Subsystem](https://www.kernel.org/doc/html/latest/gpu/drm-uapi.html) +/// - [PCI ID Database](https://pci-ids.ucw.cz/) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GpuDevice { + /// GPU index (0-based, unique per system). + pub index: u32, + + /// GPU product name. + /// + /// Examples: + /// - "NVIDIA H100 80GB HBM3" + /// - "AMD Instinct MI250X" + pub name: String, + + // ... more fields with individual documentation +} +``` + +### Enum Documentation + +```rust +/// Storage device type classification. +/// +/// Classifies storage devices by their underlying technology. +/// Used for inventory categorization and performance expectations. +/// +/// # Detection +/// +/// Type is determined by: +/// 1. Device name prefix (`nvme*`, `sd*`, `mmcblk*`) +/// 2. sysfs rotational flag +/// 3. Interface type +/// +/// # Example +/// +/// ```rust +/// use hardware_report::StorageType; +/// +/// let device_type = StorageType::from_device("nvme0n1", false); +/// assert_eq!(device_type, StorageType::Nvme); +/// +/// match device_type { +/// StorageType::Nvme | StorageType::Ssd => println!("Fast storage"), +/// StorageType::Hdd => println!("Rotational storage"), +/// _ => println!("Other"), +/// } +/// ``` +/// +/// # References +/// +/// - [Linux Block Devices](https://www.kernel.org/doc/html/latest/block/index.html) +/// - [NVMe Specification](https://nvmexpress.org/specifications/) +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub enum StorageType { + /// NVMe solid-state drive. + /// + /// Detected by `nvme*` device name prefix. + /// Typically provides highest performance (PCIe interface). + Nvme, + + /// SATA/SAS solid-state drive. + /// + /// Detected by `rotational=0` on `sd*` devices. + Ssd, + + /// Hard disk drive (rotational media). + /// + /// Detected by `rotational=1` on `sd*` devices. + Hdd, + + /// Embedded MMC storage. + /// + /// Common on ARM platforms. Detected by `mmcblk*` prefix. + Emmc, + + /// Unknown or unclassified storage type. + Unknown, +} +``` + +### Function Documentation + +```rust +/// Parse sysfs frequency file to MHz. +/// +/// Converts kernel cpufreq values (in kHz) to MHz for consistent +/// representation across the crate. +/// +/// # Arguments +/// +/// * `content` - Content of a cpufreq file (e.g., `scaling_max_freq`) +/// +/// # Returns +/// +/// Frequency in MHz as `u32`. +/// +/// # Errors +/// +/// Returns an error if the content cannot be parsed as an integer. +/// +/// # Example +/// +/// ```rust +/// use hardware_report::domain::parsers::cpu::parse_sysfs_freq_khz; +/// +/// // 3.5 GHz in kHz +/// let freq_mhz = parse_sysfs_freq_khz("3500000").unwrap(); +/// assert_eq!(freq_mhz, 3500); +/// +/// // Invalid input +/// assert!(parse_sysfs_freq_khz("invalid").is_err()); +/// ``` +/// +/// # References +/// +/// - [cpufreq sysfs](https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.rst) +pub fn parse_sysfs_freq_khz(content: &str) -> Result { + let khz: u32 = content + .trim() + .parse() + .map_err(|e| format!("Invalid frequency: {}", e))?; + Ok(khz / 1000) +} +``` + +--- + +## External References + +### Reference Link Format + +Use markdown links in the `# References` section: + +```rust +/// # References +/// +/// - [Link Text](https://full.url.here) +``` + +### Standard Reference URLs + +#### Linux Kernel + +| Topic | URL Pattern | +|-------|-------------| +| sysfs ABI | `https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-*` | +| Block devices | `https://www.kernel.org/doc/html/latest/block/index.html` | +| Networking | `https://www.kernel.org/doc/html/latest/networking/index.html` | +| DRM/GPU | `https://www.kernel.org/doc/html/latest/gpu/drm-uapi.html` | +| CPU | `https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.rst` | + +#### Hardware Specifications + +| Topic | URL | +|-------|-----| +| NVMe | `https://nvmexpress.org/specifications/` | +| SMBIOS | `https://www.dmtf.org/standards/smbios` | +| PCI IDs | `https://pci-ids.ucw.cz/` | +| JEDEC (Memory) | `https://www.jedec.org/` | + +#### Vendor Documentation + +| Vendor | Topic | URL | +|--------|-------|-----| +| NVIDIA | NVML | `https://docs.nvidia.com/deploy/nvml-api/` | +| NVIDIA | CUDA CC | `https://developer.nvidia.com/cuda-gpus` | +| AMD | ROCm SMI | `https://rocm.docs.amd.com/projects/rocm_smi_lib/en/latest/` | +| Intel | CPUID | `https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html` | +| ARM | CPU ID | `https://developer.arm.com/documentation/ddi0487/latest` | + +#### Crate Documentation + +| Crate | URL | +|-------|-----| +| nvml-wrapper | `https://docs.rs/nvml-wrapper` | +| raw-cpuid | `https://docs.rs/raw-cpuid` | +| sysinfo | `https://docs.rs/sysinfo` | +| serde | `https://docs.rs/serde` | + +### Intra-doc Links + +Use Rust's intra-doc links to reference other items in the crate: + +```rust +/// See [`GpuDevice`] for GPU information. +/// See [`StorageType::Nvme`] for NVMe detection. +/// See [`parse_sysfs_size`](crate::domain::parsers::storage::parse_sysfs_size). +``` + +--- + +## Examples + +### Testable Examples + +All examples in doc comments should be testable: + +```rust +/// # Example +/// +/// ```rust +/// use hardware_report::StorageType; +/// +/// let st = StorageType::from_device("nvme0n1", false); +/// assert_eq!(st, StorageType::Nvme); +/// ``` +``` + +Run with: +```bash +cargo test --doc +``` + +### Non-runnable Examples + +For examples that can't be run (require hardware, external commands): + +```rust +/// # Example +/// +/// ```rust,no_run +/// use hardware_report::create_service; +/// +/// #[tokio::main] +/// async fn main() { +/// let service = create_service().unwrap(); +/// let report = service.generate_report(Default::default()).await.unwrap(); +/// println!("{:?}", report); +/// } +/// ``` +``` + +### Examples That Should Not Compile + +For showing incorrect usage: + +```rust +/// # Example of what NOT to do +/// +/// ```rust,compile_fail +/// // This won't compile because memory is u64, not String +/// let memory: String = gpu.memory_total_mb; +/// ``` +``` + +--- + +## Module Documentation + +### Module-Level Documentation + +Every module should have a `//!` comment at the top: + +```rust +//! GPU information parsing functions. +//! +//! This module provides pure parsing functions for GPU information from +//! various sources. All functions take string input and return parsed +//! results without performing I/O. +//! +//! # Supported Formats +//! +//! - nvidia-smi CSV output +//! - rocm-smi JSON output +//! - lspci text output +//! - sysfs file contents +//! +//! # Architecture +//! +//! These functions are part of the **domain layer** in the hexagonal +//! architecture. They have no dependencies on adapters or I/O. +//! +//! # Example +//! +//! ```rust +//! use hardware_report::domain::parsers::gpu::parse_nvidia_smi_output; +//! +//! let output = "0, NVIDIA H100, GPU-xxx, 81920, 81000"; +//! let gpus = parse_nvidia_smi_output(output).unwrap(); +//! assert_eq!(gpus[0].memory_total_mb, 81920); +//! ``` +//! +//! # References +//! +//! - [nvidia-smi](https://developer.nvidia.com/nvidia-system-management-interface) +//! - [rocm-smi](https://rocm.docs.amd.com/projects/rocm_smi_lib/en/latest/) + +use crate::domain::{GpuDevice, GpuVendor}; + +// ... module contents +``` + +### Re-exports Documentation + +Document re-exports in `lib.rs`: + +```rust +//! # Hardware Report +//! +//! A library for collecting hardware information on Linux systems. +//! +//! ## Quick Start +//! +//! ```rust,no_run +//! use hardware_report::{create_service, ReportConfig}; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! let service = create_service()?; +//! let report = service.generate_report(ReportConfig::default()).await?; +//! println!("Hostname: {}", report.hostname); +//! Ok(()) +//! } +//! ``` +//! +//! ## Architecture +//! +//! This crate follows the Hexagonal Architecture (Ports and Adapters): +//! +//! - **Domain**: Core entities and pure parsing functions +//! - **Ports**: Trait definitions for required/provided interfaces +//! - **Adapters**: Platform-specific implementations +//! +//! ## Feature Flags +//! +//! - `nvidia` - Enable NVML support for NVIDIA GPUs +//! - `x86-cpu` - Enable raw-cpuid for x86 CPU detection + +pub use domain::entities::*; +pub use domain::errors::*; +``` + +--- + +## Linting and CI + +### Rustdoc Lints + +Enable documentation lints in `lib.rs`: + +```rust +#![warn(missing_docs)] +#![warn(rustdoc::broken_intra_doc_links)] +#![warn(rustdoc::private_intra_doc_links)] +#![warn(rustdoc::missing_crate_level_docs)] +#![warn(rustdoc::invalid_codeblock_attributes)] +#![warn(rustdoc::invalid_html_tags)] +``` + +### CI Checks + +Add to CI workflow: + +```yaml +- name: Check documentation + run: | + cargo doc --no-deps --document-private-items + cargo test --doc + +- name: Check for broken links + run: | + cargo rustdoc -- -D warnings +``` + +### Local Documentation + +Generate and view docs locally: + +```bash +# Generate docs +cargo doc --no-deps --open + +# Generate with private items +cargo doc --no-deps --document-private-items --open + +# Test doc examples +cargo test --doc +``` + +--- + +## Checklist + +Before submitting code, verify: + +- [ ] All public items have `///` doc comments +- [ ] Modules have `//!` documentation +- [ ] Examples compile and pass (`cargo test --doc`) +- [ ] External references are included where relevant +- [ ] Intra-doc links work (`cargo doc` succeeds) +- [ ] `#[deprecated]` items explain migration path +- [ ] Complex types have usage examples +- [ ] Error conditions are documented + +--- + +## Changelog + +| Date | Changes | +|------|---------| +| 2024-12-29 | Initial standards document | diff --git a/docs/STORAGE_DETECTION.md b/docs/STORAGE_DETECTION.md new file mode 100644 index 0000000..484d8fa --- /dev/null +++ b/docs/STORAGE_DETECTION.md @@ -0,0 +1,1136 @@ +# Storage Detection Enhancement Plan + +> **Category:** Critical Issue +> **Target Platforms:** Linux (x86_64, aarch64) +> **Priority:** Critical - Storage returns empty on ARM platforms + +## Table of Contents + +1. [Problem Statement](#problem-statement) +2. [Current Implementation](#current-implementation) +3. [Multi-Method Detection Strategy](#multi-method-detection-strategy) +4. [Entity Changes](#entity-changes) +5. [Detection Method Details](#detection-method-details) +6. [Adapter Implementation](#adapter-implementation) +7. [Parser Implementation](#parser-implementation) +8. [ARM/aarch64 Considerations](#armaarch64-considerations) +9. [Testing Requirements](#testing-requirements) +10. [References](#references) + +--- + +## Problem Statement + +### Current Issue + +The `hardware_report` crate returns an empty storage array on ARM/aarch64 platforms: + +```rust +// Current output on ARM +StorageInfo { + devices: [], // Empty! +} +``` + +Additionally, the current `StorageDevice` structure lacks critical fields for CMDB: + +```rust +// Current struct - missing fields +pub struct StorageDevice { + pub name: String, + pub type_: String, // String, not enum + pub size: String, // String, not numeric + pub model: String, + // Missing: serial_number, firmware_version, interface, etc. +} +``` + +### Impact + +- No storage inventory on ARM platforms (DGX Spark, Graviton, etc.) +- CMDB cannot track storage serial numbers for asset management +- No firmware version for compliance tracking +- Size as string breaks automated capacity calculations + +### Requirements + +1. **Reliable detection on ARM/aarch64** - Primary target platform +2. **Numeric size fields** - `size_bytes: u64` and `size_gb: f64` +3. **Serial number extraction** - For asset tracking (may require privileges) +4. **Firmware version** - For compliance and update tracking +5. **Multi-method fallback** - sysfs primary, lsblk secondary, sysinfo tertiary + +--- + +## Current Implementation + +### Location + +- **Entity:** `src/domain/entities.rs:207-225` +- **Adapter:** `src/adapters/secondary/system/linux.rs:153-170` +- **Parser:** `src/domain/parsers/storage.rs` + +### Current Detection Flow + +``` +┌─────────────────────────────────────────────┐ +│ LinuxSystemInfoProvider::get_storage_info() │ +└─────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────┐ + │ lsblk -d -o │ + │ NAME,SIZE,TYPE │ + └──────────────────────┘ + │ + ▼ + ┌──────────────────────┐ + │ Parse text output │ + │ (whitespace split) │ + └──────────────────────┘ + │ + ▼ + Return devices + (may be empty!) +``` + +### Why It Fails on ARM + +1. **lsblk output format differs** - Column ordering/presence varies +2. **No fallback** - If lsblk fails, no alternative tried +3. **Parsing assumes columns** - `parts[3]` for size fails if fewer columns +4. **No sysfs fallback** - Most reliable source not used + +--- + +## Multi-Method Detection Strategy + +### Detection Priority Chain + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ STORAGE DETECTION CHAIN │ +│ │ +│ Priority 1: sysfs /sys/block (Linux) │ +│ ├── Most reliable across architectures │ +│ ├── Direct kernel interface │ +│ ├── Works on x86_64 and aarch64 │ +│ ├── Serial/firmware may require elevated privileges │ +│ └── Paths: │ +│ ├── /sys/block/{dev}/size │ +│ ├── /sys/block/{dev}/device/model │ +│ ├── /sys/block/{dev}/device/serial │ +│ ├── /sys/block/{dev}/device/firmware_rev │ +│ └── /sys/block/{dev}/queue/rotational │ +│ │ │ +│ ▼ (enrich with additional data) │ +│ Priority 2: lsblk JSON output │ +│ ├── Structured output format │ +│ ├── Additional fields (FSTYPE, MOUNTPOINT) │ +│ └── Command: lsblk -J -o NAME,SIZE,TYPE,MODEL,SERIAL,ROTA │ +│ │ │ +│ ▼ (if lsblk unavailable) │ +│ Priority 3: NVMe CLI (for NVMe devices) │ +│ ├── Detailed NVMe information │ +│ ├── Firmware version │ +│ └── Command: nvme list -o json │ +│ │ │ +│ ▼ (cross-platform fallback) │ +│ Priority 4: sysinfo crate │ +│ ├── Cross-platform disk enumeration │ +│ ├── Limited metadata │ +│ └── Good for basic size/mount info │ +│ │ │ +│ ▼ (for serial numbers if other methods fail) │ +│ Priority 5: smartctl (SMART data) │ +│ ├── Serial number │ +│ ├── Firmware version │ +│ ├── Health status │ +│ └── Requires smartmontools package │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### Method Capabilities Matrix + +| Method | Size | Model | Serial | Firmware | Type | Rotational | NVMe-specific | +|--------|------|-------|--------|----------|------|------------|---------------| +| sysfs | Yes | Yes | Maybe* | Maybe* | Yes | Yes | Partial | +| lsblk | Yes | Yes | Maybe* | No | Yes | Yes | No | +| nvme-cli | Yes | Yes | Yes | Yes | NVMe only | No | Yes | +| sysinfo | Yes | No | No | No | Limited | No | No | +| smartctl | Yes | Yes | Yes | Yes | Yes | Yes | Yes | + +*Requires elevated privileges or specific kernel configuration + +--- + +## Entity Changes + +### New StorageType Enum + +```rust +// src/domain/entities.rs + +/// Storage device type classification +/// +/// Classifies storage devices by their underlying technology and interface. +/// +/// # Detection +/// +/// Type is determined by: +/// 1. Device name prefix (nvme*, sd*, mmcblk*) +/// 2. sysfs rotational flag +/// 3. Interface type from sysfs +/// +/// # References +/// +/// - [Linux Block Devices](https://www.kernel.org/doc/html/latest/block/index.html) +/// - [NVMe Specification](https://nvmexpress.org/specifications/) +/// - [SATA Specification](https://sata-io.org/developers/sata-revision-3-5-specification) +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub enum StorageType { + /// NVMe solid-state drive + /// + /// Detected by device name starting with "nvme" or interface type. + /// Typically provides highest performance. + Nvme, + + /// SATA/SAS solid-state drive + /// + /// Detected by rotational=0 on sd* devices. + Ssd, + + /// Hard disk drive (rotational media) + /// + /// Detected by rotational=1 on sd* devices. + Hdd, + + /// Embedded MMC storage + /// + /// Common on ARM platforms (eMMC). Detected by mmcblk* device name. + Emmc, + + /// Virtual or memory-backed device + /// + /// Includes RAM disks, loop devices, and device-mapper devices. + Virtual, + + /// Unknown or unclassified storage type + Unknown, +} + +impl StorageType { + /// Determine storage type from device name and rotational flag + /// + /// # Arguments + /// + /// * `device_name` - Block device name (e.g., "nvme0n1", "sda", "mmcblk0") + /// * `is_rotational` - Whether the device uses rotational media + /// + /// # Example + /// + /// ``` + /// use hardware_report::StorageType; + /// + /// assert_eq!(StorageType::from_device("nvme0n1", false), StorageType::Nvme); + /// assert_eq!(StorageType::from_device("sda", false), StorageType::Ssd); + /// assert_eq!(StorageType::from_device("sda", true), StorageType::Hdd); + /// assert_eq!(StorageType::from_device("mmcblk0", false), StorageType::Emmc); + /// ``` + pub fn from_device(device_name: &str, is_rotational: bool) -> Self { + if device_name.starts_with("nvme") { + StorageType::Nvme + } else if device_name.starts_with("mmcblk") { + StorageType::Emmc + } else if device_name.starts_with("loop") + || device_name.starts_with("ram") + || device_name.starts_with("dm-") + { + StorageType::Virtual + } else if is_rotational { + StorageType::Hdd + } else if device_name.starts_with("sd") || device_name.starts_with("vd") { + StorageType::Ssd + } else { + StorageType::Unknown + } + } + + /// Get human-readable display name + pub fn display_name(&self) -> &'static str { + match self { + StorageType::Nvme => "NVMe SSD", + StorageType::Ssd => "SSD", + StorageType::Hdd => "HDD", + StorageType::Emmc => "eMMC", + StorageType::Virtual => "Virtual", + StorageType::Unknown => "Unknown", + } + } +} + +impl std::fmt::Display for StorageType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.display_name()) + } +} +``` + +### New StorageDevice Structure + +```rust +// src/domain/entities.rs + +/// Storage device information +/// +/// Represents a block storage device detected in the system. Provides both +/// numeric and string representations of size for flexibility. +/// +/// # Detection Methods +/// +/// Storage devices are detected using multiple methods in priority order: +/// 1. **sysfs** - `/sys/block` interface (most reliable on Linux) +/// 2. **lsblk** - Block device listing command +/// 3. **nvme-cli** - NVMe-specific tooling +/// 4. **sysinfo** - Cross-platform crate fallback +/// 5. **smartctl** - SMART data for enrichment +/// +/// # Filtering +/// +/// Virtual devices (loop, ram, dm-*) are excluded by default. Use the +/// `include_virtual` configuration option to include them. +/// +/// # Privileges +/// +/// Some fields (serial_number, firmware_version) may require elevated +/// privileges (root/sudo) to read. These will be `None` if inaccessible. +/// +/// # Example +/// +/// ``` +/// use hardware_report::StorageDevice; +/// +/// // Size is available in multiple formats +/// let size_tb = device.size_bytes as f64 / (1024.0 * 1024.0 * 1024.0 * 1024.0); +/// let size_gb = device.size_gb; // Pre-calculated convenience field +/// ``` +/// +/// # References +/// +/// - [Linux sysfs block ABI](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-block) +/// - [NVMe CLI](https://github.com/linux-nvme/nvme-cli) +/// - [smartmontools](https://www.smartmontools.org/) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct StorageDevice { + /// Block device name (e.g., "nvme0n1", "sda") + /// + /// This is the kernel device name without the `/dev/` prefix. + pub name: String, + + /// Full device path (e.g., "/dev/nvme0n1") + pub device_path: String, + + /// Storage device type classification + pub device_type: StorageType, + + /// Device size in bytes + /// + /// Calculated from sysfs `size` (512-byte sectors) or other sources. + /// This is the raw capacity, not the formatted/usable capacity. + pub size_bytes: u64, + + /// Device size in gigabytes (convenience field) + /// + /// Calculated as `size_bytes / (1024^3)`. Note this uses binary GB (GiB). + pub size_gb: f64, + + /// Device size in terabytes (convenience field) + /// + /// Calculated as `size_bytes / (1024^4)`. Note this uses binary TB (TiB). + pub size_tb: f64, + + /// Device model name + /// + /// From sysfs `/sys/block/{dev}/device/model` or equivalent. + /// May include trailing whitespace from hardware. + pub model: String, + + /// Device serial number + /// + /// Important for asset tracking and CMDB inventory. + /// + /// # Note + /// + /// May require elevated privileges to read. Returns `None` if + /// inaccessible or not available. + /// + /// # Sources + /// + /// - sysfs: `/sys/block/{dev}/device/serial` + /// - NVMe: `/sys/class/nvme/{ctrl}/serial` + /// - smartctl: `smartctl -i /dev/{dev}` + pub serial_number: Option, + + /// Device firmware version + /// + /// Important for compliance tracking and identifying devices + /// that need firmware updates. + /// + /// # Sources + /// + /// - sysfs: `/sys/block/{dev}/device/firmware_rev` + /// - NVMe: `/sys/class/nvme/{ctrl}/firmware_rev` + /// - smartctl: `smartctl -i /dev/{dev}` + pub firmware_version: Option, + + /// Interface type + /// + /// Examples: "NVMe", "SATA", "SAS", "USB", "eMMC", "virtio" + pub interface: String, + + /// Whether the device uses rotational media + /// + /// - `true` = HDD (spinning platters) + /// - `false` = SSD/NVMe/eMMC (solid state) + /// + /// Read from sysfs `/sys/block/{dev}/queue/rotational`. + pub is_rotational: bool, + + /// World Wide Name (WWN) if available + /// + /// A globally unique identifier for the device. Format varies: + /// - SATA: NAA format (e.g., "0x5000c5004567890a") + /// - NVMe: EUI-64 or NGUID + /// + /// # Sources + /// + /// - sysfs: `/sys/block/{dev}/device/wwid` + /// - lsblk: WWN column + pub wwn: Option, + + /// NVMe Namespace ID (NVMe devices only) + /// + /// For NVMe devices, this identifies the namespace within the controller. + /// Typically 1 for single-namespace devices. + pub nvme_namespace: Option, + + /// SMART health status + /// + /// Indicates overall device health based on SMART data. + /// Values: "PASSED", "FAILED", or `None` if unavailable. + pub smart_status: Option, + + /// Transport protocol + /// + /// More specific than `interface`. Examples: + /// - "PCIe 4.0 x4" (NVMe) + /// - "SATA 6Gb/s" + /// - "SAS 12Gb/s" + pub transport: Option, + + /// Logical block size in bytes + /// + /// Typically 512 or 4096. Affects alignment requirements. + pub block_size: Option, + + /// Physical block size in bytes + /// + /// May differ from logical block size (e.g., 4Kn drives). + pub physical_block_size: Option, + + /// Detection method that discovered this device + /// + /// One of: "sysfs", "lsblk", "nvme-cli", "sysinfo", "smartctl" + pub detection_method: String, +} + +impl Default for StorageDevice { + fn default() -> Self { + Self { + name: String::new(), + device_path: String::new(), + device_type: StorageType::Unknown, + size_bytes: 0, + size_gb: 0.0, + size_tb: 0.0, + model: String::new(), + serial_number: None, + firmware_version: None, + interface: "Unknown".to_string(), + is_rotational: false, + wwn: None, + nvme_namespace: None, + smart_status: None, + transport: None, + block_size: None, + physical_block_size: None, + detection_method: String::new(), + } + } +} + +impl StorageDevice { + /// Calculate size fields from bytes + /// + /// Updates `size_gb` and `size_tb` based on `size_bytes`. + pub fn calculate_size_fields(&mut self) { + const GB: f64 = 1024.0 * 1024.0 * 1024.0; + const TB: f64 = GB * 1024.0; + self.size_gb = self.size_bytes as f64 / GB; + self.size_tb = self.size_bytes as f64 / TB; + } +} +``` + +--- + +## Detection Method Details + +### Method 1: sysfs /sys/block (Primary) + +**When:** Linux systems (always attempted first) + +**sysfs paths for each device:** + +``` +/sys/block/{device}/ +├── size # Size in 512-byte sectors +├── queue/ +│ ├── rotational # 0=SSD, 1=HDD +│ ├── logical_block_size # Logical block size +│ └── physical_block_size # Physical block size +├── device/ +│ ├── model # Device model (may have trailing spaces) +│ ├── vendor # Device vendor +│ ├── serial # Serial number (may need root) +│ ├── firmware_rev # Firmware version +│ └── wwid # World Wide Name +└── ... (other attributes) +``` + +**NVMe-specific paths:** + +``` +/sys/class/nvme/{controller}/ +├── serial # Controller serial number +├── model # Controller model +├── firmware_rev # Firmware revision +└── transport # Transport type (pcie, tcp, rdma) + +/sys/class/nvme/{controller}/nvme{X}n{Y}/ +├── size # Namespace size +├── wwid # Namespace WWID +└── ... +``` + +**Size calculation:** + +```rust +// sysfs reports size in 512-byte sectors +let sectors: u64 = read_sysfs_file("/sys/block/sda/size")?.parse()?; +let size_bytes = sectors * 512; +``` + +**References:** +- [sysfs-block ABI](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-block) +- [sysfs-class-nvme](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-nvme) + +--- + +### Method 2: lsblk JSON Output + +**When:** Enrichment after sysfs, or if sysfs incomplete + +**Command:** +```bash +lsblk -J -o NAME,SIZE,TYPE,MODEL,SERIAL,ROTA,TRAN,WWN,FSTYPE,MOUNTPOINT -b +``` + +**Output format:** +```json +{ + "blockdevices": [ + { + "name": "nvme0n1", + "size": 2000398934016, + "type": "disk", + "model": "Samsung SSD 980 PRO 2TB", + "serial": "S5GXNF0N123456", + "rota": false, + "tran": "nvme", + "wwn": "eui.0025385b21404321" + } + ] +} +``` + +**Note:** The `-b` flag outputs size in bytes, avoiding parsing human-readable formats. + +**References:** +- [lsblk man page](https://man7.org/linux/man-pages/man8/lsblk.8.html) +- [util-linux source](https://github.com/util-linux/util-linux) + +--- + +### Method 3: NVMe CLI + +**When:** NVMe devices detected, nvme-cli available + +**Command:** +```bash +nvme list -o json +``` + +**Output format:** +```json +{ + "Devices": [ + { + "DevicePath": "/dev/nvme0n1", + "Firmware": "1B2QGXA7", + "ModelNumber": "Samsung SSD 980 PRO 2TB", + "SerialNumber": "S5GXNF0N123456", + "PhysicalSize": 2000398934016, + "UsedBytes": 1500000000000 + } + ] +} +``` + +**References:** +- [nvme-cli GitHub](https://github.com/linux-nvme/nvme-cli) +- [NVMe Specification](https://nvmexpress.org/specifications/) + +--- + +### Method 4: sysinfo Crate + +**When:** Cross-platform fallback, or other methods unavailable + +**Usage:** +```rust +use sysinfo::Disks; + +let disks = Disks::new_with_refreshed_list(); +for disk in disks.iter() { + let name = disk.name().to_string_lossy(); + let size = disk.total_space(); + let fs_type = disk.file_system().to_string_lossy(); + let mount_point = disk.mount_point(); +} +``` + +**Limitations:** +- Reports mounted filesystems, not raw block devices +- No serial number or firmware version +- Limited device type detection + +**References:** +- [sysinfo crate](https://docs.rs/sysinfo) + +--- + +### Method 5: smartctl + +**When:** Serial/firmware needed and not available from sysfs + +**Command:** +```bash +smartctl -i /dev/sda --json +``` + +**Output format:** +```json +{ + "model_name": "Samsung SSD 870 EVO 2TB", + "serial_number": "S5XXNX0N123456", + "firmware_version": "SVT01B6Q", + "smart_status": { + "passed": true + } +} +``` + +**Note:** Requires `smartmontools` package and often root privileges. + +**References:** +- [smartmontools](https://www.smartmontools.org/) +- [smartctl man page](https://linux.die.net/man/8/smartctl) + +--- + +## Adapter Implementation + +### File: `src/adapters/secondary/system/linux.rs` + +```rust +// Pseudocode for new implementation + +impl SystemInfoProvider for LinuxSystemInfoProvider { + async fn get_storage_info(&self) -> Result { + let mut devices = Vec::new(); + + // Method 1: sysfs (primary) + match self.detect_storage_sysfs().await { + Ok(sysfs_devices) => { + log::debug!("Found {} devices via sysfs", sysfs_devices.len()); + devices = sysfs_devices; + } + Err(e) => { + log::warn!("sysfs storage detection failed: {}", e); + } + } + + // Method 2: lsblk enrichment + if let Ok(lsblk_devices) = self.detect_storage_lsblk().await { + self.merge_storage_info(&mut devices, lsblk_devices); + } + + // Method 3: NVMe CLI enrichment (for NVMe devices) + if devices.iter().any(|d| d.device_type == StorageType::Nvme) { + if let Ok(nvme_devices) = self.detect_storage_nvme_cli().await { + self.merge_storage_info(&mut devices, nvme_devices); + } + } + + // Method 4: sysinfo fallback (if no devices found) + if devices.is_empty() { + if let Ok(sysinfo_devices) = self.detect_storage_sysinfo().await { + devices = sysinfo_devices; + } + } + + // Method 5: smartctl enrichment (for missing serial/firmware) + for device in &mut devices { + if device.serial_number.is_none() || device.firmware_version.is_none() { + if let Ok(smart_info) = self.get_smart_info(&device.name).await { + self.merge_smart_info(device, smart_info); + } + } + } + + // Filter out virtual devices (configurable) + devices.retain(|d| d.device_type != StorageType::Virtual); + + // Calculate convenience fields + for device in &mut devices { + device.calculate_size_fields(); + } + + Ok(StorageInfo { devices }) + } +} +``` + +### Helper Methods + +```rust +impl LinuxSystemInfoProvider { + /// Detect storage devices via sysfs + /// + /// Primary detection method for Linux. Reads directly from + /// `/sys/block` kernel interface. + /// + /// # References + /// + /// - [sysfs-block ABI](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-block) + async fn detect_storage_sysfs(&self) -> Result, SystemError> { + // Read /sys/block directory + // For each entry, read attributes + // Build StorageDevice + todo!() + } + + /// Detect storage devices via lsblk command + /// + /// # Requirements + /// + /// - `lsblk` must be in PATH (util-linux package) + async fn detect_storage_lsblk(&self) -> Result, SystemError> { + let cmd = SystemCommand::new("lsblk") + .args(&["-J", "-o", "NAME,SIZE,TYPE,MODEL,SERIAL,ROTA,TRAN,WWN", "-b"]) + .timeout(Duration::from_secs(10)); + + let output = self.command_executor.execute(&cmd).await?; + parse_lsblk_json(&output.stdout).map_err(SystemError::ParseError) + } + + /// Detect NVMe devices via nvme-cli + /// + /// # Requirements + /// + /// - `nvme` must be in PATH (nvme-cli package) + async fn detect_storage_nvme_cli(&self) -> Result, SystemError> { + let cmd = SystemCommand::new("nvme") + .args(&["list", "-o", "json"]) + .timeout(Duration::from_secs(10)); + + let output = self.command_executor.execute(&cmd).await?; + parse_nvme_list_json(&output.stdout).map_err(SystemError::ParseError) + } + + /// Detect storage via sysinfo crate + /// + /// Cross-platform fallback with limited information. + async fn detect_storage_sysinfo(&self) -> Result, SystemError> { + use sysinfo::Disks; + + let disks = Disks::new_with_refreshed_list(); + let mut devices = Vec::new(); + + for disk in disks.iter() { + // Convert sysinfo disk to StorageDevice + // ... + } + + Ok(devices) + } + + /// Get SMART information for a device + /// + /// # Requirements + /// + /// - `smartctl` must be in PATH (smartmontools package) + /// - Often requires root privileges + async fn get_smart_info(&self, device_name: &str) -> Result { + let cmd = SystemCommand::new("smartctl") + .args(&["-i", "--json", &format!("/dev/{}", device_name)]) + .timeout(Duration::from_secs(10)); + + let output = self.command_executor.execute_with_privileges(&cmd).await?; + parse_smartctl_json(&output.stdout).map_err(SystemError::ParseError) + } + + /// Merge storage info from secondary source + /// + /// Matches devices by name and fills in missing fields. + fn merge_storage_info(&self, primary: &mut Vec, secondary: Vec) { + for sec_dev in secondary { + if let Some(pri_dev) = primary.iter_mut().find(|d| d.name == sec_dev.name) { + // Fill in missing fields + if pri_dev.serial_number.is_none() { + pri_dev.serial_number = sec_dev.serial_number; + } + if pri_dev.firmware_version.is_none() { + pri_dev.firmware_version = sec_dev.firmware_version; + } + // ... other fields + } else { + // Device not in primary, add it + primary.push(sec_dev); + } + } + } +} +``` + +--- + +## Parser Implementation + +### File: `src/domain/parsers/storage.rs` + +```rust +//! Storage information parsing functions +//! +//! This module provides pure parsing functions for storage device information +//! from various sources. All functions take string input and return parsed +//! results without performing I/O. +//! +//! # Supported Formats +//! +//! - sysfs file contents +//! - lsblk JSON output +//! - nvme-cli JSON output +//! - smartctl JSON output +//! +//! # References +//! +//! - [Linux sysfs block](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-block) +//! - [lsblk JSON format](https://github.com/util-linux/util-linux) + +use crate::domain::{StorageDevice, StorageType}; + +/// Parse sysfs size file to bytes +/// +/// # Arguments +/// +/// * `content` - Content of `/sys/block/{dev}/size` file +/// +/// # Returns +/// +/// Size in bytes. sysfs reports size in 512-byte sectors. +/// +/// # Example +/// +/// ``` +/// use hardware_report::domain::parsers::storage::parse_sysfs_size; +/// +/// let size_bytes = parse_sysfs_size("3907029168").unwrap(); +/// assert_eq!(size_bytes, 3907029168 * 512); // ~2TB +/// ``` +pub fn parse_sysfs_size(content: &str) -> Result { + let sectors: u64 = content + .trim() + .parse() + .map_err(|e| format!("Failed to parse size: {}", e))?; + Ok(sectors * 512) +} + +/// Parse sysfs rotational flag +/// +/// # Arguments +/// +/// * `content` - Content of `/sys/block/{dev}/queue/rotational` file +/// +/// # Returns +/// +/// `true` if device is rotational (HDD), `false` for SSD/NVMe. +/// +/// # Example +/// +/// ``` +/// use hardware_report::domain::parsers::storage::parse_sysfs_rotational; +/// +/// assert_eq!(parse_sysfs_rotational("1"), true); // HDD +/// assert_eq!(parse_sysfs_rotational("0"), false); // SSD +/// ``` +pub fn parse_sysfs_rotational(content: &str) -> bool { + content.trim() == "1" +} + +/// Parse lsblk JSON output +/// +/// # Arguments +/// +/// * `output` - JSON output from `lsblk -J -o NAME,SIZE,TYPE,MODEL,SERIAL,ROTA,TRAN,WWN -b` +/// +/// # Returns +/// +/// Vector of storage devices parsed from lsblk output. +/// +/// # Expected Format +/// +/// ```json +/// { +/// "blockdevices": [ +/// {"name": "sda", "size": 1000204886016, "type": "disk", ...} +/// ] +/// } +/// ``` +/// +/// # References +/// +/// - [lsblk man page](https://man7.org/linux/man-pages/man8/lsblk.8.html) +pub fn parse_lsblk_json(output: &str) -> Result, String> { + todo!() +} + +/// Parse nvme-cli list JSON output +/// +/// # Arguments +/// +/// * `output` - JSON output from `nvme list -o json` +/// +/// # Returns +/// +/// Vector of NVMe storage devices. +/// +/// # Expected Format +/// +/// ```json +/// { +/// "Devices": [ +/// {"DevicePath": "/dev/nvme0n1", "SerialNumber": "...", ...} +/// ] +/// } +/// ``` +/// +/// # References +/// +/// - [nvme-cli](https://github.com/linux-nvme/nvme-cli) +pub fn parse_nvme_list_json(output: &str) -> Result, String> { + todo!() +} + +/// Parse smartctl JSON output +/// +/// # Arguments +/// +/// * `output` - JSON output from `smartctl -i --json /dev/{device}` +/// +/// # Returns +/// +/// Partial storage device with SMART information. +/// +/// # References +/// +/// - [smartmontools](https://www.smartmontools.org/) +pub fn parse_smartctl_json(output: &str) -> Result { + todo!() +} + +/// Check if device name is a virtual device +/// +/// # Arguments +/// +/// * `name` - Block device name (e.g., "sda", "loop0", "dm-0") +/// +/// # Returns +/// +/// `true` if the device is virtual (loop, ram, dm-*, etc.) +pub fn is_virtual_device(name: &str) -> bool { + name.starts_with("loop") + || name.starts_with("ram") + || name.starts_with("dm-") + || name.starts_with("zram") + || name.starts_with("nbd") +} +``` + +--- + +## ARM/aarch64 Considerations + +### Known ARM Platforms + +| Platform | Storage Type | Notes | +|----------|--------------|-------| +| NVIDIA DGX Spark | NVMe | Grace Hopper, ARM Neoverse | +| AWS Graviton | NVMe, EBS | Various instance storage | +| Ampere Altra | NVMe | Server-class ARM | +| Raspberry Pi | SD/eMMC | mmcblk* devices | +| Apple Silicon | NVMe | Not Linux target | + +### ARM-Specific sysfs Paths + +Some ARM platforms use slightly different sysfs layouts: + +``` +# Standard path +/sys/block/nvme0n1/device/serial + +# Some ARM platforms +/sys/class/nvme/nvme0/serial + +# eMMC on ARM +/sys/block/mmcblk0/device/cid # Contains serial in CID register +``` + +### eMMC CID Parsing + +eMMC devices encode serial number in the CID (Card Identification) register: + +```rust +/// Parse eMMC CID to extract serial number +/// +/// # Arguments +/// +/// * `cid` - Content of `/sys/block/mmcblk*/device/cid` (32 hex chars) +/// +/// # References +/// +/// - [JEDEC eMMC Standard](https://www.jedec.org/) +pub fn parse_emmc_cid_serial(cid: &str) -> Option { + // CID format: MID(1) + OID(2) + PNM(6) + PRV(1) + PSN(4) + MDT(2) + CRC(1) + // PSN (Product Serial Number) is bytes 10-13 + if cid.len() < 32 { + return None; + } + let serial_hex = &cid[20..28]; // PSN bytes + Some(serial_hex.to_uppercase()) +} +``` + +### Testing on ARM + +```bash +# Test sysfs availability +ls -la /sys/block/ +cat /sys/block/*/size + +# Check for NVMe +ls -la /sys/class/nvme/ + +# Check for eMMC +ls -la /sys/block/mmcblk* +``` + +--- + +## Testing Requirements + +### Unit Tests + +| Test | Description | +|------|-------------| +| `test_parse_sysfs_size` | Parse sector count to bytes | +| `test_parse_sysfs_rotational` | Parse rotational flag | +| `test_parse_lsblk_json` | Parse lsblk JSON output | +| `test_parse_nvme_list_json` | Parse nvme-cli JSON | +| `test_parse_smartctl_json` | Parse smartctl JSON | +| `test_storage_type_from_device` | Device name to type mapping | +| `test_is_virtual_device` | Virtual device detection | +| `test_parse_emmc_cid` | eMMC CID serial extraction | + +### Integration Tests + +| Test | Platform | Description | +|------|----------|-------------| +| `test_sysfs_detection` | Linux | Full sysfs detection | +| `test_lsblk_detection` | Linux | lsblk fallback | +| `test_nvme_detection` | Linux + NVMe | NVMe-specific detection | +| `test_arm_detection` | aarch64 | ARM platform detection | +| `test_emmc_detection` | aarch64 | eMMC device detection | + +### Test Hardware Matrix + +| Platform | Storage | Test Type | +|----------|---------|-----------| +| x86_64 Linux | NVMe | CI + Manual | +| x86_64 Linux | SATA SSD | Manual | +| x86_64 Linux | HDD | Manual | +| aarch64 Linux (DGX Spark) | NVMe | Manual | +| aarch64 Linux (Graviton) | NVMe | CI | +| aarch64 Linux (RPi) | eMMC | Manual | + +--- + +## References + +### Official Documentation + +| Resource | URL | +|----------|-----| +| Linux sysfs block ABI | https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-block | +| Linux sysfs nvme ABI | https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-nvme | +| NVMe Specification | https://nvmexpress.org/specifications/ | +| JEDEC eMMC Standard | https://www.jedec.org/ | +| smartmontools | https://www.smartmontools.org/ | +| nvme-cli | https://github.com/linux-nvme/nvme-cli | +| lsblk (util-linux) | https://github.com/util-linux/util-linux | + +### Crate Documentation + +| Crate | URL | +|-------|-----| +| sysinfo | https://docs.rs/sysinfo | +| serde_json | https://docs.rs/serde_json | + +### Kernel Documentation + +| Path | Description | +|------|-------------| +| `/sys/block/` | Block device sysfs | +| `/sys/class/nvme/` | NVMe controller class | +| `/proc/partitions` | Partition information | +| `/dev/disk/by-id/` | Persistent device naming | + +--- + +## Changelog + +| Date | Changes | +|------|---------| +| 2024-12-29 | Initial specification | diff --git a/docs/TESTING_STRATEGY.md b/docs/TESTING_STRATEGY.md new file mode 100644 index 0000000..396dd6d --- /dev/null +++ b/docs/TESTING_STRATEGY.md @@ -0,0 +1,709 @@ +# Testing Strategy + +> **Target Platforms:** Linux x86_64, Linux aarch64 +> **Primary Test Target:** aarch64 (ARM64) - DGX Spark, Graviton + +## Table of Contents + +1. [Overview](#overview) +2. [Test Categories](#test-categories) +3. [Platform Matrix](#platform-matrix) +4. [Unit Testing](#unit-testing) +5. [Integration Testing](#integration-testing) +6. [Hardware Testing](#hardware-testing) +7. [CI/CD Configuration](#cicd-configuration) +8. [Test Data Management](#test-data-management) +9. [Mocking Strategy](#mocking-strategy) + +--- + +## Overview + +The `hardware_report` crate requires testing across multiple architectures and hardware configurations. This document defines the testing strategy to ensure reliability on both x86_64 and aarch64 platforms. + +### Testing Principles + +1. **Parser functions are pure** - Test with captured output, no hardware needed +2. **Adapters require mocking** - Use trait-based dependency injection +3. **Integration tests need real hardware** - Run on CI matrix or manual +4. **ARM is primary target** - Ensure full coverage on aarch64 + +--- + +## Test Categories + +### Test Pyramid + +``` + ┌─────────────────┐ + │ Manual/E2E │ ← Real hardware, manual verification + │ (5%) │ + ├─────────────────┤ + │ Integration │ ← Real sysfs, commands, CI matrix + │ (20%) │ + ├─────────────────┤ + │ Unit Tests │ ← Pure functions, mocked adapters + │ (75%) │ + └─────────────────┘ +``` + +### Test Types + +| Type | Location | Dependencies | CI | +|------|----------|--------------|-----| +| Unit | `src/**/*.rs` (inline) | None | Yes | +| Parser | `tests/parsers/` | Sample data files | Yes | +| Adapter | `tests/adapters/` | Mocked traits | Yes | +| Integration | `tests/integration/` | Real system | Matrix | +| Hardware | `tests/hardware/` | Physical hardware | Manual | + +--- + +## Platform Matrix + +### Target Platforms + +| Platform | Architecture | GPU | CI Support | Notes | +|----------|--------------|-----|------------|-------| +| Linux x86_64 | x86_64 | NVIDIA | GitHub Actions | Standard runners | +| Linux x86_64 | x86_64 | AMD | Self-hosted | Optional | +| Linux aarch64 | aarch64 | None | GitHub Actions | `ubuntu-24.04-arm` | +| Linux aarch64 | aarch64 | NVIDIA | Self-hosted | DGX Spark | +| macOS x86_64 | x86_64 | Apple | GitHub Actions | Legacy Intel | +| macOS aarch64 | aarch64 | Apple | GitHub Actions | M1/M2/M3 | + +### CI Matrix Configuration + +```yaml +strategy: + matrix: + include: + # x86_64 Linux + - os: ubuntu-latest + arch: x86_64 + target: x86_64-unknown-linux-gnu + features: "full" + + # aarch64 Linux (GitHub-hosted ARM) + - os: ubuntu-24.04-arm + arch: aarch64 + target: aarch64-unknown-linux-gnu + features: "" # No nvidia feature on ARM CI + + # Cross-compile for ARM (build only) + - os: ubuntu-latest + arch: x86_64 + target: aarch64-unknown-linux-gnu + cross: true + features: "" +``` + +--- + +## Unit Testing + +### Parser Unit Tests + +Parser functions are pure and easily testable: + +```rust +// src/domain/parsers/storage.rs + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_sysfs_size() { + // 2TB drive in 512-byte sectors + assert_eq!(parse_sysfs_size("3907029168").unwrap(), 2000398934016); + + // 1TB drive + assert_eq!(parse_sysfs_size("1953525168").unwrap(), 1000204886016); + + // Empty/whitespace + assert!(parse_sysfs_size("").is_err()); + assert!(parse_sysfs_size(" ").is_err()); + } + + #[test] + fn test_parse_sysfs_rotational() { + assert!(parse_sysfs_rotational("1")); // HDD + assert!(!parse_sysfs_rotational("0")); // SSD + assert!(!parse_sysfs_rotational("0\n")); // With newline + } + + #[test] + fn test_storage_type_from_device() { + assert_eq!(StorageType::from_device("nvme0n1", false), StorageType::Nvme); + assert_eq!(StorageType::from_device("sda", false), StorageType::Ssd); + assert_eq!(StorageType::from_device("sda", true), StorageType::Hdd); + assert_eq!(StorageType::from_device("mmcblk0", false), StorageType::Emmc); + assert_eq!(StorageType::from_device("loop0", false), StorageType::Virtual); + } +} +``` + +### GPU Parser Tests + +```rust +// src/domain/parsers/gpu.rs + +#[cfg(test)] +mod tests { + use super::*; + + const NVIDIA_SMI_OUTPUT: &str = r#"0, NVIDIA H100 80GB HBM3, GPU-12345678-1234-1234-1234-123456789abc, 81920, 81000, 00000000:01:00.0, 535.129.03, 9.0 +1, NVIDIA H100 80GB HBM3, GPU-87654321-4321-4321-4321-cba987654321, 81920, 80500, 00000000:02:00.0, 535.129.03, 9.0"#; + + #[test] + fn test_parse_nvidia_smi_output() { + let gpus = parse_nvidia_smi_output(NVIDIA_SMI_OUTPUT).unwrap(); + + assert_eq!(gpus.len(), 2); + + assert_eq!(gpus[0].index, 0); + assert_eq!(gpus[0].name, "NVIDIA H100 80GB HBM3"); + assert_eq!(gpus[0].memory_total_mb, 81920); + assert_eq!(gpus[0].memory_free_mb, Some(81000)); + assert_eq!(gpus[0].driver_version, Some("535.129.03".to_string())); + assert_eq!(gpus[0].compute_capability, Some("9.0".to_string())); + } + + #[test] + fn test_parse_nvidia_smi_empty() { + let gpus = parse_nvidia_smi_output("").unwrap(); + assert!(gpus.is_empty()); + } + + #[test] + fn test_parse_pci_vendor() { + assert_eq!(parse_pci_vendor("10de"), GpuVendor::Nvidia); + assert_eq!(parse_pci_vendor("0x10de"), GpuVendor::Nvidia); + assert_eq!(parse_pci_vendor("1002"), GpuVendor::Amd); + assert_eq!(parse_pci_vendor("8086"), GpuVendor::Intel); + assert_eq!(parse_pci_vendor("abcd"), GpuVendor::Unknown); + } +} +``` + +### CPU Parser Tests + +```rust +// src/domain/parsers/cpu.rs + +#[cfg(test)] +mod tests { + use super::*; + + const PROC_CPUINFO_INTEL: &str = r#" +processor : 0 +vendor_id : GenuineIntel +cpu family : 6 +model : 106 +model name : Intel(R) Xeon(R) Platinum 8380 CPU @ 2.30GHz +stepping : 6 +microcode : 0xd0003a5 +flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat avx avx2 avx512f avx512dq +"#; + + const PROC_CPUINFO_ARM: &str = r#" +processor : 0 +BogoMIPS : 50.00 +Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp +CPU implementer : 0x41 +CPU architecture: 8 +CPU variant : 0x3 +CPU part : 0xd0c +CPU revision : 1 +"#; + + #[test] + fn test_parse_proc_cpuinfo_intel() { + let info = parse_proc_cpuinfo(PROC_CPUINFO_INTEL).unwrap(); + + assert_eq!(info.vendor, "GenuineIntel"); + assert_eq!(info.model, "Intel(R) Xeon(R) Platinum 8380 CPU @ 2.30GHz"); + assert_eq!(info.family, Some(6)); + assert_eq!(info.model_number, Some(106)); + assert!(info.flags.contains(&"avx512f".to_string())); + } + + #[test] + fn test_parse_proc_cpuinfo_arm() { + let info = parse_proc_cpuinfo(PROC_CPUINFO_ARM).unwrap(); + + assert_eq!(info.vendor, "ARM"); + assert!(info.flags.contains(&"asimd".to_string())); + assert_eq!(info.microarchitecture, Some("Neoverse N1".to_string())); + } + + #[test] + fn test_arm_cpu_part_mapping() { + assert_eq!(arm_cpu_part_to_name("0xd0c"), Some("Neoverse N1")); + assert_eq!(arm_cpu_part_to_name("0xd40"), Some("Neoverse V1")); + assert_eq!(arm_cpu_part_to_name("0xd49"), Some("Neoverse N2")); + assert_eq!(arm_cpu_part_to_name("0xffff"), None); + } + + #[test] + fn test_parse_sysfs_freq() { + assert_eq!(parse_sysfs_freq_khz("3500000").unwrap(), 3500); + assert_eq!(parse_sysfs_freq_khz("2100000\n").unwrap(), 2100); + assert!(parse_sysfs_freq_khz("invalid").is_err()); + } + + #[test] + fn test_parse_cache_size() { + assert_eq!(parse_sysfs_cache_size("32K").unwrap(), 32); + assert_eq!(parse_sysfs_cache_size("1M").unwrap(), 1024); + assert_eq!(parse_sysfs_cache_size("256M").unwrap(), 262144); + assert_eq!(parse_sysfs_cache_size("32768K").unwrap(), 32768); + } +} +``` + +--- + +## Integration Testing + +### sysfs Integration Tests + +```rust +// tests/integration/sysfs_storage.rs + +#[cfg(target_os = "linux")] +mod tests { + use std::fs; + use std::path::Path; + + #[test] + fn test_sysfs_block_exists() { + assert!(Path::new("/sys/block").exists()); + } + + #[test] + fn test_can_read_block_devices() { + let entries = fs::read_dir("/sys/block").unwrap(); + let devices: Vec<_> = entries + .filter_map(|e| e.ok()) + .map(|e| e.file_name().to_string_lossy().to_string()) + .filter(|n| !n.starts_with("loop") && !n.starts_with("ram")) + .collect(); + + // Most systems have at least one real block device + // This may fail in minimal containers + println!("Found block devices: {:?}", devices); + } + + #[test] + fn test_can_read_device_size() { + if let Ok(entries) = fs::read_dir("/sys/block") { + for entry in entries.flatten() { + let name = entry.file_name(); + let size_path = format!("/sys/block/{}/size", name.to_string_lossy()); + + if let Ok(size_str) = fs::read_to_string(&size_path) { + let sectors: u64 = size_str.trim().parse().unwrap_or(0); + let bytes = sectors * 512; + println!("{}: {} bytes", name.to_string_lossy(), bytes); + } + } + } + } +} +``` + +### Command Execution Tests + +```rust +// tests/integration/commands.rs + +#[cfg(target_os = "linux")] +mod tests { + use std::process::Command; + + #[test] + fn test_lsblk_available() { + let output = Command::new("which").arg("lsblk").output(); + assert!(output.is_ok()); + } + + #[test] + fn test_lsblk_json_output() { + let output = Command::new("lsblk") + .args(["-J", "-o", "NAME,SIZE,TYPE"]) + .output(); + + match output { + Ok(out) if out.status.success() => { + let stdout = String::from_utf8_lossy(&out.stdout); + assert!(stdout.contains("blockdevices")); + } + _ => { + // lsblk may not be available in all environments + println!("lsblk not available, skipping"); + } + } + } + + #[test] + #[cfg(target_arch = "aarch64")] + fn test_arm_specific_detection() { + // Verify ARM-specific paths exist + let cpuinfo = std::fs::read_to_string("/proc/cpuinfo").unwrap(); + + // ARM cpuinfo has different format + assert!( + cpuinfo.contains("CPU implementer") || + cpuinfo.contains("model name"), + "Expected ARM or x86 CPU info format" + ); + } +} +``` + +--- + +## Hardware Testing + +### Manual Test Checklist + +#### Storage Tests + +```bash +# Run on target hardware +cargo test --test storage_hardware -- --ignored + +# Expected output validation: +# - At least one storage device detected +# - Size > 0 for all devices +# - Type correctly identified (NVMe/SSD/HDD) +# - Serial number present (may need sudo) +``` + +#### GPU Tests + +```bash +# Run on NVIDIA system +cargo test --test gpu_hardware --features nvidia -- --ignored + +# Expected output validation: +# - All GPUs detected +# - Memory matches nvidia-smi +# - Driver version present +# - PCI bus ID present +``` + +### Hardware Test Files + +```rust +// tests/hardware/storage.rs + +#[test] +#[ignore] // Run manually on real hardware +fn test_storage_detection_real_hardware() { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + let service = hardware_report::create_service().unwrap(); + let config = hardware_report::ReportConfig::default(); + let report = service.generate_report(config).await.unwrap(); + + // Validate storage + assert!(!report.hardware.storage.devices.is_empty(), + "No storage devices detected"); + + for device in &report.hardware.storage.devices { + assert!(device.size_bytes > 0, + "Device {} has zero size", device.name); + assert!(!device.model.is_empty(), + "Device {} has empty model", device.name); + println!("Found: {} - {} - {} GB", + device.name, device.model, device.size_gb); + } + }); +} + +#[test] +#[ignore] +#[cfg(target_arch = "aarch64")] +fn test_arm_storage_detection() { + // ARM-specific storage test + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + let service = hardware_report::create_service().unwrap(); + let config = hardware_report::ReportConfig::default(); + let report = service.generate_report(config).await.unwrap(); + + // On ARM, we should detect NVMe or eMMC + assert!(!report.hardware.storage.devices.is_empty(), + "No storage on ARM - sysfs fallback may have failed"); + + let has_nvme = report.hardware.storage.devices.iter() + .any(|d| d.device_type == StorageType::Nvme); + let has_emmc = report.hardware.storage.devices.iter() + .any(|d| d.device_type == StorageType::Emmc); + + println!("ARM storage: NVMe={}, eMMC={}", has_nvme, has_emmc); + }); +} +``` + +--- + +## CI/CD Configuration + +### GitHub Actions Workflow + +```yaml +# .github/workflows/test.yml + +name: Test + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + test-x86: + name: Test x86_64 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-action@stable + + - name: Run unit tests + run: cargo test --lib --all-features + + - name: Run doc tests + run: cargo test --doc + + - name: Run integration tests + run: cargo test --test '*' -- --skip hardware + + test-arm: + name: Test aarch64 + runs-on: ubuntu-24.04-arm + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-action@stable + + - name: Run unit tests + run: cargo test --lib + + - name: Run ARM integration tests + run: cargo test --test '*' -- --skip hardware + + - name: Test ARM-specific code paths + run: | + # Verify ARM detection works + cargo run --example basic_usage 2>&1 | tee output.txt + grep -q "architecture.*aarch64" output.txt || echo "Warning: arch detection may have issues" + + cross-compile: + name: Cross-compile check + runs-on: ubuntu-latest + strategy: + matrix: + target: + - aarch64-unknown-linux-gnu + - aarch64-unknown-linux-musl + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-action@stable + with: + targets: ${{ matrix.target }} + + - name: Install cross + run: cargo install cross + + - name: Cross build + run: cross build --target ${{ matrix.target }} --release + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-action@stable + with: + components: clippy, rustfmt + + - name: Check formatting + run: cargo fmt --check + + - name: Clippy + run: cargo clippy --all-features -- -D warnings + + - name: Check docs + run: cargo doc --no-deps --all-features + env: + RUSTDOCFLAGS: -D warnings +``` + +--- + +## Test Data Management + +### Sample Data Files + +Store captured command outputs for parser testing: + +``` +tests/ +├── data/ +│ ├── nvidia-smi/ +│ │ ├── h100-8gpu.csv +│ │ ├── a100-4gpu.csv +│ │ └── no-gpu.csv +│ ├── lsblk/ +│ │ ├── nvme-only.json +│ │ ├── mixed-storage.json +│ │ └── arm-emmc.json +│ ├── proc/ +│ │ ├── cpuinfo-intel-xeon.txt +│ │ ├── cpuinfo-amd-epyc.txt +│ │ └── cpuinfo-arm-neoverse.txt +│ └── sysfs/ +│ ├── block-nvme/ +│ └── cpu-arm/ +``` + +### Loading Test Data + +```rust +// tests/common/mod.rs + +use std::path::PathBuf; + +pub fn test_data_path(relative: &str) -> PathBuf { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/data"); + path.push(relative); + path +} + +pub fn load_test_data(relative: &str) -> String { + std::fs::read_to_string(test_data_path(relative)) + .expect(&format!("Failed to load test data: {}", relative)) +} + +// Usage in tests: +#[test] +fn test_nvidia_h100_parsing() { + let data = load_test_data("nvidia-smi/h100-8gpu.csv"); + let gpus = parse_nvidia_smi_output(&data).unwrap(); + assert_eq!(gpus.len(), 8); +} +``` + +--- + +## Mocking Strategy + +### Trait-Based Mocking + +```rust +// src/ports/secondary/system.rs - The trait + +#[async_trait] +pub trait SystemInfoProvider: Send + Sync { + async fn get_storage_info(&self) -> Result; + async fn get_gpu_info(&self) -> Result; + // ... +} + +// tests/mocks/system.rs - Mock implementation + +pub struct MockSystemInfoProvider { + pub storage_result: Result, + pub gpu_result: Result, +} + +#[async_trait] +impl SystemInfoProvider for MockSystemInfoProvider { + async fn get_storage_info(&self) -> Result { + self.storage_result.clone() + } + + async fn get_gpu_info(&self) -> Result { + self.gpu_result.clone() + } +} + +// Usage in tests +#[tokio::test] +async fn test_service_with_mock() { + let mock = MockSystemInfoProvider { + storage_result: Ok(StorageInfo { + devices: vec![ + StorageDevice { + name: "nvme0n1".to_string(), + size_bytes: 1000204886016, + ..Default::default() + } + ] + }), + gpu_result: Ok(GpuInfo { devices: vec![] }), + }; + + // Inject mock into service + let service = HardwareCollectionService::new(Arc::new(mock)); + let report = service.generate_report(Default::default()).await.unwrap(); + + assert_eq!(report.hardware.storage.devices.len(), 1); +} +``` + +### Command Executor Mocking + +```rust +// Mock command executor for testing adapter logic + +pub struct MockCommandExecutor { + pub responses: HashMap, +} + +impl MockCommandExecutor { + pub fn new() -> Self { + Self { responses: HashMap::new() } + } + + pub fn mock_command(&mut self, cmd: &str, result: CommandResult) { + self.responses.insert(cmd.to_string(), result); + } +} + +#[async_trait] +impl CommandExecutor for MockCommandExecutor { + async fn execute(&self, cmd: &SystemCommand) -> Result { + if let Some(result) = self.responses.get(&cmd.program) { + Ok(result.clone()) + } else { + Err(CommandError::NotFound(cmd.program.clone())) + } + } +} +``` + +--- + +## Changelog + +| Date | Changes | +|------|---------| +| 2024-12-29 | Initial testing strategy | From 3028fc1cfca54e7f0906be86612531664dfd54f7 Mon Sep 17 00:00:00 2001 From: "Kenny (Knight) Sheridan" Date: Tue, 30 Dec 2025 13:55:09 -0800 Subject: [PATCH 2/8] added StorageDevice --- docs/IMPLEMENTATION_GUIDE.md | 3679 ++++++++++++++++++++++++++++++++++ src/domain/entities.rs | 161 +- 2 files changed, 3813 insertions(+), 27 deletions(-) create mode 100644 docs/IMPLEMENTATION_GUIDE.md diff --git a/docs/IMPLEMENTATION_GUIDE.md b/docs/IMPLEMENTATION_GUIDE.md new file mode 100644 index 0000000..1c73ee8 --- /dev/null +++ b/docs/IMPLEMENTATION_GUIDE.md @@ -0,0 +1,3679 @@ +# Implementation Guide: Learn by Doing + +> **Purpose:** Step-by-step implementation guide with LeetCode patterns and real-world connections +> **Learning Style:** Type it yourself with detailed explanations + +## Table of Contents + +1. [Overview](#overview) +2. [LeetCode Patterns Used](#leetcode-patterns-used) +3. [Implementation Order](#implementation-order) +4. [Step 1: Storage Enhancements](#step-1-storage-enhancements) +5. [Step 2: CPU Enhancements](#step-2-cpu-enhancements) +6. [Step 3: GPU Enhancements](#step-3-gpu-enhancements) +7. [Step 4: Memory Enhancements](#step-4-memory-enhancements) +8. [Step 5: Network Enhancements](#step-5-network-enhancements) +9. [Step 6: Cargo.toml Updates](#step-6-cargotoml-updates) + +--- + +## Overview + +This guide walks you through implementing each enhancement with: +- **Commented code** explaining every decision +- **LeetCode pattern callouts** showing real-world applications +- **Why it matters** for CMDB/inventory systems + +### How to Use This Guide + +1. Read each section's explanation +2. Type the code yourself (don't copy-paste!) +3. Run `cargo check` after each change +4. Run `cargo test` to verify +5. Understand the LeetCode pattern connection + +--- + +## LeetCode Patterns Used + +This project uses several classic algorithm patterns. Here's how they map: + +| Pattern | LeetCode Examples | Where Used Here | +|---------|-------------------|-----------------| +| **Chain of Responsibility** | - | Multi-method detection (try method 1, fallback to 2, etc.) | +| **Strategy Pattern** | - | Different parsers for different data sources | +| **Builder Pattern** | - | Constructing complex structs with defaults | +| **Two Pointers / Sliding Window** | LC #3, #76, #567 | Parsing delimited strings | +| **Hash Map for Lookups** | LC #1, #49, #242 | PCI vendor ID → vendor name mapping | +| **Tree/Graph Traversal** | LC #200, #547 | Walking sysfs directory tree | +| **String Parsing** | LC #8, #65, #468 | Parsing nvidia-smi output, sysfs files | +| **Merge/Combine Data** | LC #56, #88 | Merging GPU info from multiple sources | +| **Filter/Transform** | LC #283, #27 | Filtering virtual devices, transforming sizes | +| **State Machine** | LC #65, #10 | Parsing multi-line dmidecode output | +| **Adapter Pattern** | - | Platform-specific implementations behind traits | + +--- + +## Implementation Order + +Follow this exact order to avoid compilation errors: + +``` +1. entities.rs - Add new types (StorageType, GpuVendor, etc.) +2. parsers/*.rs - Add parsing functions (pure, no I/O) +3. linux.rs - Update adapter to use new parsers +4. Cargo.toml - Add new dependencies (if needed) +5. tests - Verify everything works +``` + +**Why this order?** +- Entities are dependencies for everything else +- Parsers depend only on entities (pure functions) +- Adapters depend on both entities and parsers +- Tests depend on all of the above + +--- + +## Step 1: Storage Enhancements + +### 1.1 Add StorageType Enum to entities.rs + +**File:** `src/domain/entities.rs` + +**Where:** Add after line 205 (after MemoryModule), before StorageInfo + +**LeetCode Pattern:** This is similar to **categorization problems** like LC #49 (Group Anagrams) +where you classify items into buckets. Here we classify storage devices by type. + +```rust +// ============================================================================= +// STORAGE TYPE ENUM +// ============================================================================= +// +// WHY: We need to categorize storage devices so CMDB consumers can: +// 1. Filter by type (show only SSDs) +// 2. Calculate capacity by category +// 3. Apply different monitoring thresholds +// +// LEETCODE CONNECTION: This is the "categorization" pattern seen in: +// - LC #49 Group Anagrams: group strings by sorted chars +// - LC #347 Top K Frequent: group by frequency +// - Here: group storage by technology type +// +// PATTERN: Enum with associated functions for classification +// ============================================================================= + +/// Storage device type classification. +/// +/// Classifies storage devices by their underlying technology and interface. +/// This enables filtering, capacity planning, and performance expectations. +/// +/// # Detection Logic +/// +/// Type is determined by examining (in order): +/// 1. Device name prefix (`nvme*` → NVMe, `mmcblk*` → eMMC) +/// 2. sysfs rotational flag (`0` = solid state, `1` = spinning) +/// 3. Interface type from sysfs +/// +/// # Example +/// +/// ```rust +/// use hardware_report::StorageType; +/// +/// // Classify based on device name and rotational flag +/// let nvme = StorageType::from_device("nvme0n1", false); +/// assert_eq!(nvme, StorageType::Nvme); +/// +/// let hdd = StorageType::from_device("sda", true); // rotational=1 +/// assert_eq!(hdd, StorageType::Hdd); +/// +/// let ssd = StorageType::from_device("sda", false); // rotational=0 +/// assert_eq!(ssd, StorageType::Ssd); +/// ``` +/// +/// # References +/// +/// - [Linux Block Devices](https://www.kernel.org/doc/html/latest/block/index.html) +/// - [NVMe Specification](https://nvmexpress.org/specifications/) +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub enum StorageType { + /// NVMe solid-state drive (PCIe interface). + /// + /// Highest performance storage. Detected by `nvme*` device name prefix. + /// Uses PCIe lanes directly, bypassing SATA/SAS bottlenecks. + Nvme, + + /// SATA/SAS solid-state drive. + /// + /// Detected by `rotational=0` on `sd*` devices. + /// Limited by SATA (6 Gbps) or SAS (12/24 Gbps) interface. + Ssd, + + /// Hard disk drive (rotational/spinning media). + /// + /// Detected by `rotational=1` on `sd*` devices. + /// Mechanical seek time limits random I/O performance. + Hdd, + + /// Embedded MMC storage. + /// + /// Common on ARM platforms (Raspberry Pi, embedded systems). + /// Detected by `mmcblk*` device name prefix. + Emmc, + + /// Virtual or memory-backed device. + /// + /// Includes loop devices, RAM disks, device-mapper. + /// Usually filtered out for hardware inventory. + Virtual, + + /// Unknown or unclassified storage type. + Unknown, +} + +// ============================================================================= +// IMPLEMENTATION: StorageType classification logic +// ============================================================================= +// +// LEETCODE CONNECTION: This classification logic is similar to: +// - LC #68 Text Justification: pattern matching on input +// - LC #722 Remove Comments: state-based string analysis +// +// The pattern here is: examine input characteristics → map to category +// ============================================================================= + +impl StorageType { + /// Determine storage type from device name and rotational flag. + /// + /// This implements a decision tree: + /// ```text + /// device_name + /// │ + /// ┌───────────────┼───────────────┐ + /// ▼ ▼ ▼ + /// nvme* mmcblk* other + /// │ │ │ + /// ▼ ▼ ▼ + /// Nvme Emmc is_rotational? + /// │ + /// ┌────────┴────────┐ + /// ▼ ▼ + /// true false + /// │ │ + /// ▼ ▼ + /// Hdd Ssd + /// ``` + /// + /// # Arguments + /// + /// * `device_name` - Block device name (e.g., "nvme0n1", "sda", "mmcblk0") + /// * `is_rotational` - Whether device uses rotational media (from sysfs) + /// + /// # Why This Order Matters + /// + /// We check name prefixes FIRST because: + /// 1. NVMe devices always report rotational=0, but we want specific type + /// 2. eMMC devices may not have rotational flag + /// 3. Name-based detection is most reliable + pub fn from_device(device_name: &str, is_rotational: bool) -> Self { + // STEP 1: Check for NVMe (highest priority, most specific) + // NVMe devices are named nvme{controller}n{namespace} + // Example: nvme0n1, nvme1n1 + if device_name.starts_with("nvme") { + return StorageType::Nvme; + } + + // STEP 2: Check for eMMC (common on ARM) + // eMMC devices are named mmcblk{N} + // Example: mmcblk0, mmcblk1 + if device_name.starts_with("mmcblk") { + return StorageType::Emmc; + } + + // STEP 3: Check for virtual devices (filter these out usually) + // These are not physical hardware + if device_name.starts_with("loop") // Loop devices (ISO mounts, etc.) + || device_name.starts_with("ram") // RAM disks + || device_name.starts_with("dm-") // Device mapper (LVM, LUKS) + || device_name.starts_with("zram") // Compressed RAM swap + || device_name.starts_with("nbd") // Network block device + { + return StorageType::Virtual; + } + + // STEP 4: For sd* and vd* devices, use rotational flag + // sd* = SCSI/SATA/SAS devices + // vd* = VirtIO devices (VMs) + if is_rotational { + StorageType::Hdd + } else if device_name.starts_with("sd") || device_name.starts_with("vd") { + StorageType::Ssd + } else { + StorageType::Unknown + } + } + + /// Get human-readable display name. + /// + /// Useful for CLI output and logging. + pub fn display_name(&self) -> &'static str { + match self { + StorageType::Nvme => "NVMe SSD", + StorageType::Ssd => "SSD", + StorageType::Hdd => "HDD", + StorageType::Emmc => "eMMC", + StorageType::Virtual => "Virtual", + StorageType::Unknown => "Unknown", + } + } + + /// Check if this is a solid-state device (no moving parts). + /// + /// Useful for performance expectations and wear-leveling considerations. + pub fn is_solid_state(&self) -> bool { + matches!(self, StorageType::Nvme | StorageType::Ssd | StorageType::Emmc) + } +} + +// Implement Display trait for easy printing +// This allows: println!("{}", storage_type); +impl std::fmt::Display for StorageType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.display_name()) + } +} +``` + +### 1.2 Update StorageDevice Struct + +**File:** `src/domain/entities.rs` + +**Where:** Replace the existing `StorageDevice` struct (around line 214-225) + +**LeetCode Pattern:** This uses the **Builder Pattern** concept where we have +many optional fields with sensible defaults. Similar to how LC #146 LRU Cache +needs to track multiple pieces of state for each entry. + +```rust +// ============================================================================= +// STORAGE DEVICE STRUCT +// ============================================================================= +// +// WHY: The old struct had: +// - type_: String → Hard to filter/compare +// - size: String → "500 GB" can't be summed or compared +// - No serial/firmware for asset tracking +// +// NEW: We add: +// - device_type: StorageType enum → Easy filtering +// - size_bytes: u64 → Math works! +// - serial_number, firmware_version → Asset tracking +// +// LEETCODE CONNECTION: This is like the "design" problems: +// - LC #146 LRU Cache: track multiple attributes per entry +// - LC #380 Insert Delete GetRandom: need efficient lookups +// - Here: need efficient queries by type, size, serial +// ============================================================================= + +/// Storage device information. +/// +/// Represents a block storage device with comprehensive metadata for +/// CMDB inventory, capacity planning, and asset tracking. +/// +/// # Detection Methods +/// +/// Storage devices are detected using multiple methods (Chain of Responsibility): +/// 1. **sysfs** `/sys/block` - Primary, most reliable on Linux +/// 2. **lsblk** - Structured command output +/// 3. **nvme-cli** - NVMe-specific details +/// 4. **sysinfo** - Cross-platform fallback +/// 5. **smartctl** - SMART data enrichment +/// +/// # Size Fields +/// +/// Size is provided in multiple formats for convenience: +/// - `size_bytes` - Raw bytes (use for calculations) +/// - `size_gb` - Gigabytes as float (use for display) +/// - `size_tb` - Terabytes as float (use for large arrays) +/// +/// # Example +/// +/// ```rust +/// use hardware_report::{StorageDevice, StorageType}; +/// +/// let device = StorageDevice { +/// name: "nvme0n1".to_string(), +/// device_type: StorageType::Nvme, +/// size_bytes: 2_000_398_934_016, // ~2TB +/// ..Default::default() +/// }; +/// +/// // Calculate total across devices +/// let devices = vec![device]; +/// let total_tb: f64 = devices.iter() +/// .map(|d| d.size_bytes as f64) +/// .sum::() / (1024.0_f64.powi(4)); +/// ``` +/// +/// # References +/// +/// - [Linux sysfs block](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-block) +/// - [NVMe CLI](https://github.com/linux-nvme/nvme-cli) +/// - [smartmontools](https://www.smartmontools.org/) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct StorageDevice { + // ========================================================================= + // IDENTIFICATION FIELDS + // ========================================================================= + + /// Block device name without /dev/ prefix. + /// + /// Examples: "nvme0n1", "sda", "mmcblk0" + /// + /// This is the kernel's name for the device, found in /sys/block/ + pub name: String, + + /// Full device path. + /// + /// Example: "/dev/nvme0n1" + /// + /// Use this when you need to open/read the device. + #[serde(default)] + pub device_path: String, + + // ========================================================================= + // TYPE AND CLASSIFICATION + // ========================================================================= + + /// Storage device type classification. + /// + /// Use this for filtering and categorization. + /// This is the NEW preferred field. + #[serde(default)] + pub device_type: StorageType, + + /// Legacy type field as string. + /// + /// DEPRECATED: Use `device_type` instead. + /// Kept for backward compatibility with existing consumers. + #[serde(rename = "type")] + pub type_: String, + + // ========================================================================= + // SIZE FIELDS + // ========================================================================= + // + // LEETCODE CONNECTION: Having multiple representations is like + // LC #273 Integer to English Words - same data, different formats + // ========================================================================= + + /// Device size in bytes. + /// + /// PRIMARY SIZE FIELD - use this for calculations. + /// + /// Calculated from sysfs: sectors × 512 (sector size) + #[serde(default)] + pub size_bytes: u64, + + /// Device size in gigabytes (binary, 1 GB = 1024³ bytes). + /// + /// Convenience field for display. Pre-calculated from size_bytes. + #[serde(default)] + pub size_gb: f64, + + /// Device size in terabytes (binary, 1 TB = 1024⁴ bytes). + /// + /// Convenience field for large storage arrays. + #[serde(default)] + pub size_tb: f64, + + /// Legacy size as human-readable string. + /// + /// DEPRECATED: Use `size_bytes` for calculations. + /// Example: "2 TB", "500 GB" + pub size: String, + + // ========================================================================= + // HARDWARE IDENTIFICATION + // ========================================================================= + + /// Device model name. + /// + /// From sysfs `/sys/block/{dev}/device/model` + /// May have trailing whitespace (hardware quirk). + /// + /// Example: "Samsung SSD 980 PRO 2TB" + pub model: String, + + /// Device serial number. + /// + /// IMPORTANT for asset tracking and warranty. + /// + /// May require elevated privileges to read. + /// Sources: + /// - sysfs: `/sys/block/{dev}/device/serial` + /// - NVMe: `/sys/class/nvme/{ctrl}/serial` + /// - smartctl: `smartctl -i /dev/{dev}` + #[serde(default, skip_serializing_if = "Option::is_none")] + pub serial_number: Option, + + /// Device firmware version. + /// + /// IMPORTANT for compliance and update tracking. + /// + /// Sources: + /// - sysfs: `/sys/block/{dev}/device/firmware_rev` + /// - NVMe: `/sys/class/nvme/{ctrl}/firmware_rev` + #[serde(default, skip_serializing_if = "Option::is_none")] + pub firmware_version: Option, + + // ========================================================================= + // INTERFACE AND TRANSPORT + // ========================================================================= + + /// Interface type. + /// + /// Examples: "NVMe", "SATA", "SAS", "USB", "eMMC", "virtio" + #[serde(default)] + pub interface: String, + + /// Whether device uses rotational media. + /// + /// - `true` = HDD (spinning platters, mechanical seek) + /// - `false` = SSD/NVMe (solid state, no moving parts) + /// + /// From sysfs: `/sys/block/{dev}/queue/rotational` + #[serde(default)] + pub is_rotational: bool, + + /// World Wide Name (globally unique identifier). + /// + /// More persistent than serial in some cases. + /// Format varies by protocol (NAA, EUI-64, NGUID). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub wwn: Option, + + // ========================================================================= + // NVME-SPECIFIC FIELDS + // ========================================================================= + + /// NVMe namespace ID (NVMe devices only). + /// + /// Identifies the namespace within the NVMe controller. + /// Most consumer drives have a single namespace (1). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub nvme_namespace: Option, + + // ========================================================================= + // HEALTH AND MONITORING + // ========================================================================= + + /// SMART health status. + /// + /// Values: "PASSED", "FAILED", or None if unavailable. + /// Requires smartctl or NVMe health query. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub smart_status: Option, + + // ========================================================================= + // BLOCK SIZE INFORMATION + // ========================================================================= + // + // LEETCODE CONNECTION: Block sizes matter for alignment, similar to + // LC #68 Text Justification where you need proper boundaries + // ========================================================================= + + /// Logical block size in bytes. + /// + /// Typically 512 or 4096. Affects I/O alignment. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub logical_block_size: Option, + + /// Physical block size in bytes. + /// + /// May differ from logical (512e drives report 512 logical, 4096 physical). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub physical_block_size: Option, + + // ========================================================================= + // METADATA + // ========================================================================= + + /// Detection method that discovered this device. + /// + /// Values: "sysfs", "lsblk", "nvme-cli", "sysinfo", "smartctl" + /// + /// Useful for debugging and understanding data quality. + #[serde(default)] + pub detection_method: String, +} + +// ============================================================================= +// DEFAULT IMPLEMENTATION +// ============================================================================= +// +// LEETCODE CONNECTION: Default/Builder pattern is used in many design problems +// LC #146 LRU Cache, LC #355 Design Twitter - initialize with sensible defaults +// ============================================================================= + +impl Default for StorageType { + fn default() -> Self { + StorageType::Unknown + } +} + +impl Default for StorageDevice { + fn default() -> Self { + Self { + name: String::new(), + device_path: String::new(), + device_type: StorageType::Unknown, + type_: String::new(), + size_bytes: 0, + size_gb: 0.0, + size_tb: 0.0, + size: String::new(), + model: String::new(), + serial_number: None, + firmware_version: None, + interface: "Unknown".to_string(), + is_rotational: false, + wwn: None, + nvme_namespace: None, + smart_status: None, + logical_block_size: None, + physical_block_size: None, + detection_method: String::new(), + } + } +} + +impl StorageDevice { + /// Calculate size_gb and size_tb from size_bytes. + /// + /// Call this after setting size_bytes to populate convenience fields. + /// + /// # Example + /// + /// ```rust + /// let mut device = StorageDevice::default(); + /// device.size_bytes = 1_000_000_000_000; // 1 TB in bytes + /// device.calculate_size_fields(); + /// assert!((device.size_gb - 931.32).abs() < 0.01); // Binary GB + /// ``` + pub fn calculate_size_fields(&mut self) { + // Use binary units (1024-based) as is standard for storage + const KB: f64 = 1024.0; + const GB: f64 = KB * KB * KB; // 1,073,741,824 + const TB: f64 = KB * KB * KB * KB; // 1,099,511,627,776 + + self.size_gb = self.size_bytes as f64 / GB; + self.size_tb = self.size_bytes as f64 / TB; + + // Also set the legacy string field + if self.size_tb >= 1.0 { + self.size = format!("{:.2} TB", self.size_tb); + } else if self.size_gb >= 1.0 { + self.size = format!("{:.2} GB", self.size_gb); + } else { + self.size = format!("{} bytes", self.size_bytes); + } + } + + /// Create device path from name. + /// + /// Convenience method to set device_path from name. + pub fn set_device_path(&mut self) { + if !self.name.is_empty() && self.device_path.is_empty() { + self.device_path = format!("/dev/{}", self.name); + } + } +} +``` + +### 1.3 Add Storage Parser Functions + +**File:** `src/domain/parsers/storage.rs` + +**Where:** Add these functions to the existing file + +**LeetCode Pattern:** String parsing here is like LC #8 (String to Integer), +LC #468 (Validate IP Address), and LC #65 (Valid Number) - parsing structured +text with edge cases. + +```rust +// ============================================================================= +// STORAGE PARSER FUNCTIONS +// ============================================================================= +// +// These are PURE FUNCTIONS - they take strings in, return parsed data out. +// No I/O, no side effects. This makes them easy to test. +// +// ARCHITECTURE: These live in the DOMAIN layer (ports and adapters pattern) +// The ADAPTER layer (linux.rs) calls these after reading from sysfs/commands. +// ============================================================================= + +use crate::domain::{StorageDevice, StorageType}; + +// ============================================================================= +// SYSFS SIZE PARSING +// ============================================================================= +// +// LEETCODE CONNECTION: This is classic string-to-number parsing like: +// - LC #8 String to Integer (atoi) +// - LC #7 Reverse Integer +// +// Key insight: sysfs reports sizes in 512-byte SECTORS, not bytes! +// ============================================================================= + +/// Parse sysfs size file to bytes. +/// +/// The Linux kernel reports block device sizes in 512-byte sectors, +/// regardless of the actual hardware sector size. +/// +/// # Arguments +/// +/// * `content` - Content of `/sys/block/{dev}/size` file +/// +/// # Returns +/// +/// Size in bytes as u64. +/// +/// # Formula +/// +/// ```text +/// size_bytes = sectors × 512 +/// ``` +/// +/// # Example +/// +/// ```rust +/// use hardware_report::domain::parsers::storage::parse_sysfs_size; +/// +/// // A 2TB drive has approximately 3.9 billion sectors +/// let size = parse_sysfs_size("3907029168").unwrap(); +/// assert_eq!(size, 3907029168 * 512); // ~2TB +/// +/// // Handle whitespace (sysfs files often have trailing newline) +/// let size = parse_sysfs_size("1000000\n").unwrap(); +/// assert_eq!(size, 1000000 * 512); +/// ``` +/// +/// # Errors +/// +/// Returns error if content is not a valid integer. +/// +/// # References +/// +/// - [sysfs block size](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-block) +pub fn parse_sysfs_size(content: &str) -> Result { + // STEP 1: Trim whitespace (sysfs files have trailing newlines) + let trimmed = content.trim(); + + // STEP 2: Parse as u64 + // Using parse::() which handles the conversion + let sectors: u64 = trimmed + .parse() + .map_err(|e| format!("Failed to parse sector count '{}': {}", trimmed, e))?; + + // STEP 3: Convert sectors to bytes + // Kernel ALWAYS uses 512-byte sectors for this file + const SECTOR_SIZE: u64 = 512; + Ok(sectors * SECTOR_SIZE) +} + +// ============================================================================= +// ROTATIONAL FLAG PARSING +// ============================================================================= +// +// LEETCODE CONNECTION: Simple boolean parsing, but demonstrates +// defensive programming - handle unexpected inputs gracefully. +// ============================================================================= + +/// Parse sysfs rotational flag. +/// +/// # Arguments +/// +/// * `content` - Content of `/sys/block/{dev}/queue/rotational` +/// +/// # Returns +/// +/// - `true` if device is rotational (HDD) +/// - `false` if solid-state (SSD, NVMe) +/// +/// # Why This Matters +/// +/// Rotational devices have: +/// - Mechanical seek latency (milliseconds vs microseconds) +/// - Sequential access is much faster than random +/// - Different SMART attributes +/// +/// # Example +/// +/// ```rust +/// use hardware_report::domain::parsers::storage::parse_sysfs_rotational; +/// +/// assert!(parse_sysfs_rotational("1")); // HDD +/// assert!(!parse_sysfs_rotational("0")); // SSD +/// assert!(!parse_sysfs_rotational("0\n")); // With newline +/// assert!(!parse_sysfs_rotational("")); // Empty = assume SSD +/// ``` +pub fn parse_sysfs_rotational(content: &str) -> bool { + // Only "1" means rotational; anything else (0, empty, error) = non-rotational + content.trim() == "1" +} + +// ============================================================================= +// LSBLK JSON PARSING +// ============================================================================= +// +// LEETCODE CONNECTION: JSON parsing is like tree traversal (LC #94, #144) +// We navigate a nested structure to extract values. +// +// Also similar to LC #1 Two Sum - we're doing key lookups in a map. +// ============================================================================= + +/// Parse lsblk JSON output into storage devices. +/// +/// # Arguments +/// +/// * `output` - JSON output from `lsblk -J -o NAME,SIZE,TYPE,MODEL,SERIAL,ROTA,TRAN,WWN -b` +/// +/// # Expected Format +/// +/// ```json +/// { +/// "blockdevices": [ +/// { +/// "name": "nvme0n1", +/// "size": 2000398934016, +/// "type": "disk", +/// "model": "Samsung SSD 980 PRO 2TB", +/// "serial": "S5GXNF0N123456", +/// "rota": false, +/// "tran": "nvme", +/// "wwn": "eui.0025385b21404321" +/// } +/// ] +/// } +/// ``` +/// +/// # Notes +/// +/// - Use `-b` flag to get size in bytes (not human-readable) +/// - `rota` is boolean (false = SSD, true = HDD) +/// - `tran` is transport type (nvme, sata, usb, etc.) +/// +/// # References +/// +/// - [lsblk man page](https://man7.org/linux/man-pages/man8/lsblk.8.html) +pub fn parse_lsblk_json(output: &str) -> Result, String> { + // STEP 1: Parse JSON + // Using serde_json which is already a dependency + let json: serde_json::Value = serde_json::from_str(output) + .map_err(|e| format!("Failed to parse lsblk JSON: {}", e))?; + + // STEP 2: Navigate to blockdevices array + // This is like tree traversal - we're finding a specific node + let devices_array = json + .get("blockdevices") + .and_then(|v| v.as_array()) + .ok_or_else(|| "Missing 'blockdevices' array in lsblk output".to_string())?; + + // STEP 3: Transform each JSON object into StorageDevice + // LEETCODE CONNECTION: This is the "transform" pattern seen in many problems + // Like LC #2 Add Two Numbers - transform input format to output format + let mut devices = Vec::new(); + + for device_json in devices_array { + // Skip non-disk entries (partitions, etc.) + let device_type_str = device_json + .get("type") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + if device_type_str != "disk" { + continue; + } + + // Extract fields with defaults for missing values + let name = device_json + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + // Skip virtual devices + if is_virtual_device(&name) { + continue; + } + + let size_bytes = device_json + .get("size") + .and_then(|v| v.as_u64()) + .unwrap_or(0); + + let model = device_json + .get("model") + .and_then(|v| v.as_str()) + .unwrap_or("") + .trim() // Models often have trailing whitespace + .to_string(); + + let serial = device_json + .get("serial") + .and_then(|v| v.as_str()) + .map(|s| s.trim().to_string()); + + let is_rotational = device_json + .get("rota") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let transport = device_json + .get("tran") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + let wwn = device_json + .get("wwn") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + // Determine storage type + let device_type = StorageType::from_device(&name, is_rotational); + + // Determine interface from transport + let interface = match transport.as_str() { + "nvme" => "NVMe".to_string(), + "sata" => "SATA".to_string(), + "sas" => "SAS".to_string(), + "usb" => "USB".to_string(), + "" => device_type.display_name().to_string(), + other => other.to_uppercase(), + }; + + // Build the device struct + let mut device = StorageDevice { + name: name.clone(), + device_path: format!("/dev/{}", name), + device_type, + type_: device_type.display_name().to_string(), + size_bytes, + model, + serial_number: serial, + interface, + is_rotational, + wwn, + detection_method: "lsblk".to_string(), + ..Default::default() + }; + + // Calculate convenience fields + device.calculate_size_fields(); + + devices.push(device); + } + + Ok(devices) +} + +// ============================================================================= +// VIRTUAL DEVICE DETECTION +// ============================================================================= +// +// LEETCODE CONNECTION: This is pattern matching, similar to: +// - LC #10 Regular Expression Matching +// - LC #44 Wildcard Matching +// +// We're checking if a string matches any of several patterns. +// ============================================================================= + +/// Check if device name indicates a virtual device. +/// +/// Virtual devices are not physical hardware and should usually be +/// filtered out of hardware inventory. +/// +/// # Arguments +/// +/// * `name` - Block device name +/// +/// # Returns +/// +/// `true` if device is virtual (loop, ram, dm-*, etc.) +/// +/// # Virtual Device Types +/// +/// | Prefix | Description | +/// |--------|-------------| +/// | loop | Loop devices (mounted ISO files, etc.) | +/// | ram | RAM disks | +/// | dm- | Device mapper (LVM, LUKS encryption) | +/// | zram | Compressed RAM for swap | +/// | nbd | Network block device | +/// +/// # Example +/// +/// ```rust +/// use hardware_report::domain::parsers::storage::is_virtual_device; +/// +/// assert!(is_virtual_device("loop0")); +/// assert!(is_virtual_device("dm-0")); +/// assert!(!is_virtual_device("sda")); +/// assert!(!is_virtual_device("nvme0n1")); +/// ``` +pub fn is_virtual_device(name: &str) -> bool { + // Check prefixes that indicate virtual devices + // Order doesn't matter for correctness, but put common ones first for efficiency + name.starts_with("loop") + || name.starts_with("dm-") + || name.starts_with("ram") + || name.starts_with("zram") + || name.starts_with("nbd") + || name.starts_with("sr") // CD/DVD drives (virtual in VMs) +} + +// ============================================================================= +// HUMAN-READABLE SIZE PARSING +// ============================================================================= +// +// LEETCODE CONNECTION: This is like LC #8 (atoi) but with unit suffixes. +// We need to handle: "500 GB", "2 TB", "1.5 TB", etc. +// +// Pattern: Parse number + parse unit + multiply +// ============================================================================= + +/// Parse human-readable size string to bytes. +/// +/// Handles common size formats from various tools. +/// +/// # Supported Formats +/// +/// - "500 GB", "500GB", "500G" +/// - "2 TB", "2TB", "2T" +/// - "1.5 TB" +/// - "1000000000" (raw bytes) +/// +/// # Units (Binary) +/// +/// - K/KB = 1024 +/// - M/MB = 1024² +/// - G/GB = 1024³ +/// - T/TB = 1024⁴ +/// +/// # Example +/// +/// ```rust +/// use hardware_report::domain::parsers::storage::parse_size_string; +/// +/// assert_eq!(parse_size_string("500 GB"), Some(500 * 1024_u64.pow(3))); +/// assert_eq!(parse_size_string("2 TB"), Some(2 * 1024_u64.pow(4))); +/// assert_eq!(parse_size_string("1.5 TB"), Some((1.5 * 1024_f64.powi(4)) as u64)); +/// ``` +pub fn parse_size_string(size_str: &str) -> Option { + let s = size_str.trim().to_uppercase(); + + // Handle "No Module Installed" or similar + if s.contains("NO ") || s.contains("UNKNOWN") || s.is_empty() { + return None; + } + + // Try to parse as raw number first + if let Ok(bytes) = s.parse::() { + return Some(bytes); + } + + // PATTERN: Split into number and unit + // "500 GB" -> ["500", "GB"] + // "500GB" -> need to find where number ends + + // Find where the number part ends + let num_end = s + .chars() + .position(|c| !c.is_ascii_digit() && c != '.') + .unwrap_or(s.len()); + + if num_end == 0 { + return None; + } + + let num_str = &s[..num_end]; + let unit_str = s[num_end..].trim(); + + // Parse the number (could be float like "1.5") + let num: f64 = num_str.parse().ok()?; + + // Determine multiplier based on unit + const KB: u64 = 1024; + const MB: u64 = KB * 1024; + const GB: u64 = MB * 1024; + const TB: u64 = GB * 1024; + + let multiplier = match unit_str { + "K" | "KB" | "KIB" => KB, + "M" | "MB" | "MIB" => MB, + "G" | "GB" | "GIB" => GB, + "T" | "TB" | "TIB" => TB, + "B" | "" => 1, + _ => return None, + }; + + Some((num * multiplier as f64) as u64) +} + +// ============================================================================= +// UNIT TESTS +// ============================================================================= +// +// IMPORTANT: Always test pure functions! They're easy to test +// because they have no dependencies. +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_sysfs_size() { + // 2TB drive (approximately 3.9 billion sectors) + assert_eq!( + parse_sysfs_size("3907029168").unwrap(), + 3907029168 * 512 + ); + + // With whitespace + assert_eq!( + parse_sysfs_size(" 1000000\n").unwrap(), + 1000000 * 512 + ); + + // Error case + assert!(parse_sysfs_size("not a number").is_err()); + assert!(parse_sysfs_size("").is_err()); + } + + #[test] + fn test_parse_sysfs_rotational() { + assert!(parse_sysfs_rotational("1")); + assert!(!parse_sysfs_rotational("0")); + assert!(!parse_sysfs_rotational("0\n")); + assert!(!parse_sysfs_rotational("")); + assert!(!parse_sysfs_rotational("garbage")); + } + + #[test] + fn test_is_virtual_device() { + // Virtual devices + assert!(is_virtual_device("loop0")); + assert!(is_virtual_device("loop1")); + assert!(is_virtual_device("dm-0")); + assert!(is_virtual_device("dm-1")); + assert!(is_virtual_device("ram0")); + assert!(is_virtual_device("zram0")); + assert!(is_virtual_device("nbd0")); + + // Physical devices + assert!(!is_virtual_device("sda")); + assert!(!is_virtual_device("sdb")); + assert!(!is_virtual_device("nvme0n1")); + assert!(!is_virtual_device("mmcblk0")); + } + + #[test] + fn test_storage_type_from_device() { + // NVMe + assert_eq!(StorageType::from_device("nvme0n1", false), StorageType::Nvme); + assert_eq!(StorageType::from_device("nvme1n1", false), StorageType::Nvme); + + // eMMC + assert_eq!(StorageType::from_device("mmcblk0", false), StorageType::Emmc); + + // Virtual + assert_eq!(StorageType::from_device("loop0", false), StorageType::Virtual); + assert_eq!(StorageType::from_device("dm-0", false), StorageType::Virtual); + + // SSD vs HDD (based on rotational flag) + assert_eq!(StorageType::from_device("sda", false), StorageType::Ssd); + assert_eq!(StorageType::from_device("sda", true), StorageType::Hdd); + } + + #[test] + fn test_parse_size_string() { + // GB + assert_eq!(parse_size_string("500 GB"), Some(500 * 1024_u64.pow(3))); + assert_eq!(parse_size_string("500GB"), Some(500 * 1024_u64.pow(3))); + + // TB + assert_eq!(parse_size_string("2 TB"), Some(2 * 1024_u64.pow(4))); + + // Raw bytes + assert_eq!(parse_size_string("1073741824"), Some(1073741824)); + + // Invalid + assert_eq!(parse_size_string("Unknown"), None); + assert_eq!(parse_size_string(""), None); + } +} +``` + +### 1.4 Update Linux Adapter for Storage + +**File:** `src/adapters/secondary/system/linux.rs` + +**Where:** Replace/update the `get_storage_info` method + +**LeetCode Pattern:** This implements **Chain of Responsibility** - we try multiple +detection methods in sequence until one succeeds. Similar to how you might try +multiple algorithms for optimization. + +```rust +// ============================================================================= +// STORAGE DETECTION IN LINUX ADAPTER +// ============================================================================= +// +// ARCHITECTURE: This is the ADAPTER layer implementation of SystemInfoProvider. +// It implements the PORT (trait) using Linux-specific mechanisms. +// +// PATTERN: Chain of Responsibility +// - Try sysfs first (most reliable) +// - Fall back to lsblk if sysfs fails +// - Use sysinfo as last resort +// +// LEETCODE CONNECTION: This pattern is used when you have multiple approaches: +// - LC #70 Climbing Stairs: try 1 step, try 2 steps +// - LC #322 Coin Change: try each coin denomination +// - Here: try each detection method +// ============================================================================= + +// Add these imports at the top of linux.rs +use crate::domain::parsers::storage::{ + parse_sysfs_size, parse_sysfs_rotational, parse_lsblk_json, is_virtual_device +}; +use crate::domain::{StorageDevice, StorageInfo, StorageType}; +use std::fs; +use std::path::Path; + +impl SystemInfoProvider for LinuxSystemInfoProvider { + // ... other methods ... + + /// Detect storage devices using multiple methods. + /// + /// # Detection Chain + /// + /// ```text + /// ┌─────────────────────────────────────────────────────────┐ + /// │ 1. sysfs /sys/block (PRIMARY) │ + /// │ - Most reliable │ + /// │ - Works on all Linux (x86, ARM) │ + /// │ - Direct kernel interface │ + /// └───────────────────────┬─────────────────────────────────┘ + /// │ enrich with + /// ▼ + /// ┌─────────────────────────────────────────────────────────┐ + /// │ 2. lsblk JSON (ENRICHMENT) │ + /// │ - Additional fields (WWN, transport) │ + /// │ - Serial number (may be available) │ + /// └───────────────────────┬─────────────────────────────────┘ + /// │ if empty, fallback + /// ▼ + /// ┌─────────────────────────────────────────────────────────┐ + /// │ 3. sysinfo crate (FALLBACK) │ + /// │ - Cross-platform │ + /// │ - Limited metadata │ + /// └─────────────────────────────────────────────────────────┘ + /// ``` + async fn get_storage_info(&self) -> Result { + let mut devices = Vec::new(); + + // ===================================================================== + // METHOD 1: sysfs (Primary - most reliable) + // ===================================================================== + // + // WHY SYSFS FIRST? + // - Direct kernel interface, always available on Linux + // - Doesn't require external tools (lsblk might not be installed) + // - Works identically on x86 and ARM + // ===================================================================== + + match self.detect_storage_sysfs().await { + Ok(sysfs_devices) => { + log::debug!("sysfs detected {} storage devices", sysfs_devices.len()); + devices = sysfs_devices; + } + Err(e) => { + log::warn!("sysfs storage detection failed: {}", e); + } + } + + // ===================================================================== + // METHOD 2: lsblk enrichment + // ===================================================================== + // + // Even if sysfs worked, lsblk might have additional data (WWN, etc.) + // We MERGE the results rather than replace. + // + // LEETCODE CONNECTION: Merging data is like: + // - LC #88 Merge Sorted Array + // - LC #21 Merge Two Sorted Lists + // Key insight: match by device name, then combine fields + // ===================================================================== + + if let Ok(lsblk_devices) = self.detect_storage_lsblk().await { + log::debug!("lsblk detected {} devices for enrichment", lsblk_devices.len()); + merge_storage_info(&mut devices, lsblk_devices); + } + + // ===================================================================== + // METHOD 3: sysinfo fallback + // ===================================================================== + // + // If we still have no devices, try sysinfo as last resort. + // This can happen in containers or unusual environments. + // ===================================================================== + + if devices.is_empty() { + log::warn!("No devices from sysfs/lsblk, trying sysinfo fallback"); + if let Ok(sysinfo_devices) = self.detect_storage_sysinfo().await { + devices = sysinfo_devices; + } + } + + // ===================================================================== + // POST-PROCESSING + // ===================================================================== + + // Filter out virtual devices (they're not physical hardware) + devices.retain(|d| d.device_type != StorageType::Virtual); + + // Ensure all devices have calculated size fields + for device in &mut devices { + if device.size_gb == 0.0 && device.size_bytes > 0 { + device.calculate_size_fields(); + } + device.set_device_path(); + } + + // Sort by name for consistent output + devices.sort_by(|a, b| a.name.cmp(&b.name)); + + Ok(StorageInfo { devices }) + } +} + +// ============================================================================= +// HELPER METHODS FOR STORAGE DETECTION +// ============================================================================= + +impl LinuxSystemInfoProvider { + /// Detect storage devices via sysfs. + /// + /// Reads directly from `/sys/block` which is the kernel's view of + /// block devices. + /// + /// # sysfs Structure + /// + /// ```text + /// /sys/block/{device}/ + /// ├── size # Size in 512-byte sectors + /// ├── queue/ + /// │ └── rotational # 0=SSD, 1=HDD + /// └── device/ + /// ├── model # Device model name + /// ├── serial # Serial number (may need root) + /// └── firmware_rev # Firmware version + /// ``` + /// + /// # Returns + /// + /// Vector of storage devices found in sysfs. + async fn detect_storage_sysfs(&self) -> Result, SystemError> { + let mut devices = Vec::new(); + + // Path to block devices in sysfs + let sys_block = Path::new("/sys/block"); + + if !sys_block.exists() { + return Err(SystemError::NotAvailable { + resource: "/sys/block".to_string(), + }); + } + + // LEETCODE CONNECTION: This is directory traversal, similar to: + // - LC #200 Number of Islands (grid traversal) + // - LC #130 Surrounded Regions + // We're walking a filesystem tree + + let entries = fs::read_dir(sys_block).map_err(|e| SystemError::IoError { + path: "/sys/block".to_string(), + message: e.to_string(), + })?; + + for entry in entries.flatten() { + let device_name = entry.file_name().to_string_lossy().to_string(); + + // Skip virtual devices early (no need to read their attributes) + if is_virtual_device(&device_name) { + continue; + } + + let device_path = entry.path(); + + // Read size (required - skip device if we can't get size) + let size_path = device_path.join("size"); + let size_bytes = match fs::read_to_string(&size_path) { + Ok(content) => match parse_sysfs_size(&content) { + Ok(size) => size, + Err(_) => continue, // Skip devices we can't parse + }, + Err(_) => continue, + }; + + // Skip tiny devices (< 1GB, probably not real storage) + if size_bytes < 1_000_000_000 { + continue; + } + + // Read rotational flag + let rotational_path = device_path.join("queue/rotational"); + let is_rotational = fs::read_to_string(&rotational_path) + .map(|content| parse_sysfs_rotational(&content)) + .unwrap_or(false); + + // Determine device type + let device_type = StorageType::from_device(&device_name, is_rotational); + + // Read model (in device subdirectory) + let model = self.read_sysfs_string(&device_path.join("device/model")) + .unwrap_or_default() + .trim() + .to_string(); + + // Read serial (may require root) + let serial_number = self.read_sysfs_string(&device_path.join("device/serial")) + .map(|s| s.trim().to_string()) + .ok(); + + // Read firmware version + let firmware_version = self.read_sysfs_string(&device_path.join("device/firmware_rev")) + .map(|s| s.trim().to_string()) + .ok(); + + // Determine interface based on device type + let interface = match &device_type { + StorageType::Nvme => "NVMe".to_string(), + StorageType::Emmc => "eMMC".to_string(), + StorageType::Hdd | StorageType::Ssd => { + // Could check for SAS vs SATA here + "SATA".to_string() + } + _ => "Unknown".to_string(), + }; + + // Build the device + let mut device = StorageDevice { + name: device_name.clone(), + device_path: format!("/dev/{}", device_name), + device_type: device_type.clone(), + type_: device_type.display_name().to_string(), + size_bytes, + model, + serial_number, + firmware_version, + interface, + is_rotational, + detection_method: "sysfs".to_string(), + ..Default::default() + }; + + device.calculate_size_fields(); + devices.push(device); + } + + Ok(devices) + } + + /// Detect storage via lsblk command. + /// + /// Uses JSON output for reliable parsing. + async fn detect_storage_lsblk(&self) -> Result, SystemError> { + let cmd = SystemCommand::new("lsblk") + .args(&[ + "-J", // JSON output + "-b", // Size in bytes + "-o", "NAME,SIZE,TYPE,MODEL,SERIAL,ROTA,TRAN,WWN", + ]) + .timeout(Duration::from_secs(10)); + + let output = self.command_executor.execute(&cmd).await.map_err(|e| { + SystemError::CommandFailed { + command: "lsblk".to_string(), + exit_code: None, + stderr: e.to_string(), + } + })?; + + if !output.success { + return Err(SystemError::CommandFailed { + command: "lsblk".to_string(), + exit_code: output.exit_code, + stderr: output.stderr.clone(), + }); + } + + parse_lsblk_json(&output.stdout).map_err(SystemError::ParseError) + } + + /// Detect storage via sysinfo crate (cross-platform fallback). + async fn detect_storage_sysinfo(&self) -> Result, SystemError> { + use sysinfo::Disks; + + let disks = Disks::new_with_refreshed_list(); + let mut devices = Vec::new(); + + for disk in disks.iter() { + let name = disk.name().to_string_lossy().to_string(); + let size_bytes = disk.total_space(); + + // Skip small/virtual + if size_bytes < 1_000_000_000 { + continue; + } + + let mut device = StorageDevice { + name: if name.is_empty() { + disk.mount_point().to_string_lossy().to_string() + } else { + name + }, + size_bytes, + detection_method: "sysinfo".to_string(), + ..Default::default() + }; + + device.calculate_size_fields(); + devices.push(device); + } + + Ok(devices) + } + + /// Helper to read a sysfs file as string. + fn read_sysfs_string(&self, path: &Path) -> Result { + fs::read_to_string(path) + } +} + +// ============================================================================= +// MERGE FUNCTION +// ============================================================================= +// +// LEETCODE CONNECTION: This is the merge pattern from: +// - LC #88 Merge Sorted Array +// - LC #56 Merge Intervals +// +// Key insight: We match by device name, then update fields that are missing +// in the primary source but present in the secondary. +// ============================================================================= + +/// Merge storage info from secondary source into primary. +/// +/// Matches devices by name and fills in missing fields. +/// +/// # Why Merge? +/// +/// Different detection methods provide different data: +/// - sysfs: reliable size, rotational flag +/// - lsblk: WWN, transport type +/// - smartctl: serial, SMART status +/// +/// By merging, we get the best of all sources. +fn merge_storage_info(primary: &mut Vec, secondary: Vec) { + // LEETCODE CONNECTION: This is O(n*m) where n = primary.len(), m = secondary.len() + // Could optimize with HashMap for O(n+m) if lists are large + // + // For small lists (typically < 20 devices), linear search is fine + + for sec_device in secondary { + // Find matching device in primary by name + if let Some(pri_device) = primary.iter_mut().find(|d| d.name == sec_device.name) { + // Fill in missing fields from secondary + // Only update if primary field is empty/None + + if pri_device.serial_number.is_none() { + pri_device.serial_number = sec_device.serial_number; + } + + if pri_device.firmware_version.is_none() { + pri_device.firmware_version = sec_device.firmware_version; + } + + if pri_device.wwn.is_none() { + pri_device.wwn = sec_device.wwn; + } + + if pri_device.model.is_empty() && !sec_device.model.is_empty() { + pri_device.model = sec_device.model; + } + } else { + // Device not in primary - add it + // This handles cases where sysfs missed a device but lsblk found it + primary.push(sec_device); + } + } +} +``` + +--- + +## Step 2: CPU Enhancements + +### 2.1 Update CpuInfo Struct + +**File:** `src/domain/entities.rs` + +**Where:** Replace the existing `CpuInfo` struct (around line 163-175) + +**LeetCode Pattern:** The cache hierarchy (L1/L2/L3) is a tree structure. Understanding +cache levels is similar to tree level traversal (LC #102, #107). + +```rust +// ============================================================================= +// CPU CACHE INFO STRUCT +// ============================================================================= +// +// WHY: CPU caches are hierarchical (L1 → L2 → L3), each with different +// characteristics. Understanding this is like understanding tree levels. +// +// LEETCODE CONNECTION: Cache hierarchy is like tree levels: +// - LC #102 Binary Tree Level Order Traversal +// - LC #107 Binary Tree Level Order Traversal II +// - L1 = leaf level (fastest, smallest) +// - L3 = root level (slowest, largest) +// ============================================================================= + +/// CPU cache level information. +/// +/// Represents a single cache level (L1d, L1i, L2, L3). +/// Each core has its own L1/L2, while L3 is typically shared. +/// +/// # Cache Hierarchy +/// +/// ```text +/// ┌─────────────────────┐ +/// │ L3 Cache │ ← Shared across cores +/// │ (8-256 MB) │ Slowest but largest +/// └──────────┬──────────┘ +/// │ +/// ┌───────────────────┼───────────────────┐ +/// │ │ │ +/// ┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐ +/// │ L2 Cache │ │ L2 Cache │ │ L2 Cache │ +/// │ (256KB-1MB) │ │ (per core) │ │ │ +/// └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ +/// │ │ │ +/// ┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐ +/// │ L1d │ L1i │ │ L1d │ L1i │ │ L1d │ L1i │ +/// │(32KB each) │ │ (per core) │ │ │ +/// └─────────────┘ └─────────────┘ └─────────────┘ +/// Core 0 Core 1 Core N +/// ``` +/// +/// # References +/// +/// - [CPU Cache Wikipedia](https://en.wikipedia.org/wiki/CPU_cache) +/// - [Linux cache sysfs](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-system-cpu) +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct CpuCacheInfo { + /// Cache level (1, 2, or 3). + pub level: u8, + + /// Cache type. + /// + /// Values: "Data" (L1d), "Instruction" (L1i), "Unified" (L2, L3) + pub cache_type: String, + + /// Cache size in kilobytes. + pub size_kb: u32, + + /// Number of ways of associativity. + /// + /// Higher = more flexible but complex. + /// Common values: 4, 8, 12, 16 + #[serde(skip_serializing_if = "Option::is_none")] + pub ways_of_associativity: Option, + + /// Cache line size in bytes. + /// + /// Typically 64 bytes on modern CPUs. + /// Important for avoiding false sharing. + #[serde(skip_serializing_if = "Option::is_none")] + pub line_size_bytes: Option, + + /// Number of sets. + #[serde(skip_serializing_if = "Option::is_none")] + pub sets: Option, + + /// Whether this cache is shared across cores. + #[serde(skip_serializing_if = "Option::is_none")] + pub shared: Option, +} + +// ============================================================================= +// CPU INFO STRUCT +// ============================================================================= +// +// WHY: The old struct had: +// - speed: String → Can't sort/compare CPUs by frequency +// - No cache info → Missing important performance data +// - No architecture → Can't distinguish x86 from ARM +// +// LEETCODE CONNECTION: CPU topology is a tree: +// - System → Sockets → Cores → Threads +// - Similar to LC #429 N-ary Tree Level Order Traversal +// ============================================================================= + +/// CPU information with extended details. +/// +/// Provides comprehensive CPU information including frequency, +/// cache hierarchy, and feature flags. +/// +/// # Detection Methods +/// +/// Information is gathered from multiple sources (Chain of Responsibility): +/// 1. **sysfs** `/sys/devices/system/cpu` - Frequency, cache +/// 2. **raw-cpuid** - CPUID instruction (x86 only) +/// 3. **/proc/cpuinfo** - Model, vendor, flags +/// 4. **lscpu** - Topology +/// 5. **dmidecode** - SMBIOS data +/// 6. **sysinfo** - Cross-platform fallback +/// +/// # Topology +/// +/// ```text +/// System +/// └── Socket 0 (physical CPU package) +/// ├── Core 0 +/// │ ├── Thread 0 (logical CPU 0) +/// │ └── Thread 1 (logical CPU 1, if SMT/HT enabled) +/// └── Core 1 +/// ├── Thread 0 (logical CPU 2) +/// └── Thread 1 (logical CPU 3) +/// └── Socket 1 (if multi-socket) +/// └── ... +/// ``` +/// +/// # Example +/// +/// ```rust +/// use hardware_report::CpuInfo; +/// +/// // Check if CPU has AVX-512 for vectorized workloads +/// let has_avx512 = cpu.flags.iter().any(|f| f.starts_with("avx512")); +/// +/// // Calculate total compute units +/// let total_threads = cpu.sockets * cpu.cores * cpu.threads; +/// ``` +/// +/// # References +/// +/// - [Linux CPU sysfs](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-system-cpu) +/// - [Intel CPUID](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) +/// - [ARM CPU ID](https://developer.arm.com/documentation/ddi0487/latest) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpuInfo { + // ========================================================================= + // IDENTIFICATION + // ========================================================================= + + /// CPU model name. + /// + /// Examples: + /// - "AMD EPYC 7763 64-Core Processor" + /// - "Intel(R) Xeon(R) Platinum 8380 CPU @ 2.30GHz" + /// - "Neoverse-N1" (ARM) + pub model: String, + + /// CPU vendor identifier. + /// + /// Values: + /// - "GenuineIntel" (Intel) + /// - "AuthenticAMD" (AMD) + /// - "ARM" (ARM-based) + #[serde(default)] + pub vendor: String, + + // ========================================================================= + // TOPOLOGY + // ========================================================================= + // + // LEETCODE CONNECTION: Understanding topology is like tree traversal + // total_threads = sockets × cores × threads_per_core + // ========================================================================= + + /// Physical cores per socket. + pub cores: u32, + + /// Threads per core (SMT/Hyperthreading). + /// + /// Usually 1 (no SMT) or 2 (SMT enabled). + pub threads: u32, + + /// Number of CPU sockets. + /// + /// Desktop: 1, Server: 1-8 + pub sockets: u32, + + /// Total physical cores (cores × sockets). + #[serde(default)] + pub total_cores: u32, + + /// Total logical CPUs (cores × threads × sockets). + #[serde(default)] + pub total_threads: u32, + + // ========================================================================= + // FREQUENCY + // ========================================================================= + // + // WHY MULTIPLE FREQUENCIES? + // - base = guaranteed frequency + // - max = turbo/boost frequency (brief bursts) + // - min = power-saving frequency + // ========================================================================= + + /// CPU frequency in MHz. + /// + /// This is the PRIMARY frequency field (current or max). + /// Use for CMDB inventory and general reporting. + #[serde(default)] + pub frequency_mhz: u32, + + /// Legacy speed field as string. + /// + /// DEPRECATED: Use `frequency_mhz` instead. + pub speed: String, + + /// Minimum scaling frequency in MHz. + /// + /// From cpufreq scaling_min_freq (power saving). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub frequency_min_mhz: Option, + + /// Maximum scaling frequency in MHz. + /// + /// From cpufreq scaling_max_freq (turbo/boost). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub frequency_max_mhz: Option, + + /// Base (non-turbo) frequency in MHz. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub frequency_base_mhz: Option, + + // ========================================================================= + // ARCHITECTURE + // ========================================================================= + + /// CPU architecture. + /// + /// Values: "x86_64", "aarch64", "armv7l" + #[serde(default)] + pub architecture: String, + + /// CPU microarchitecture name. + /// + /// Examples: + /// - Intel: "Ice Lake", "Sapphire Rapids" + /// - AMD: "Zen3", "Zen4" + /// - ARM: "Neoverse N1", "Neoverse V2" + #[serde(default, skip_serializing_if = "Option::is_none")] + pub microarchitecture: Option, + + // ========================================================================= + // CACHE SIZES + // ========================================================================= + // + // WHY SEPARATE L1d AND L1i? + // - L1d = data cache (for variables, arrays) + // - L1i = instruction cache (for code) + // - They're accessed differently, may have different sizes + // ========================================================================= + + /// L1 data cache size in KB (per core). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cache_l1d_kb: Option, + + /// L1 instruction cache size in KB (per core). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cache_l1i_kb: Option, + + /// L2 cache size in KB (usually per core). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cache_l2_kb: Option, + + /// L3 cache size in KB (usually shared). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cache_l3_kb: Option, + + /// Detailed cache information. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub caches: Vec, + + // ========================================================================= + // FEATURES AND FLAGS + // ========================================================================= + // + // LEETCODE CONNECTION: Checking flags is like set membership (LC #217) + // "Does this CPU support AVX-512?" = "Is avx512f in the set?" + // ========================================================================= + + /// CPU feature flags. + /// + /// x86 examples: "avx", "avx2", "avx512f", "aes", "sse4_2" + /// ARM examples: "fp", "asimd", "sve", "sve2" + /// + /// # Usage + /// + /// ```rust + /// // Check for AVX-512 support + /// let has_avx512 = cpu.flags.iter().any(|f| f.starts_with("avx512")); + /// + /// // Check for AES-NI (hardware encryption) + /// let has_aes = cpu.flags.contains(&"aes".to_string()); + /// ``` + #[serde(default)] + pub flags: Vec, + + // ========================================================================= + // ADDITIONAL METADATA + // ========================================================================= + + /// Microcode/firmware version. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub microcode_version: Option, + + /// CPU stepping (silicon revision). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub stepping: Option, + + /// CPU family number. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub family: Option, + + /// CPU model number (not the name). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub model_number: Option, + + /// Virtualization technology. + /// + /// Values: "VT-x" (Intel), "AMD-V" (AMD), "none" + #[serde(default, skip_serializing_if = "Option::is_none")] + pub virtualization: Option, + + /// Number of NUMA nodes. + #[serde(default)] + pub numa_nodes: u32, + + /// Detection methods used. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub detection_methods: Vec, +} + +impl Default for CpuInfo { + fn default() -> Self { + Self { + model: String::new(), + vendor: String::new(), + cores: 0, + threads: 1, + sockets: 1, + total_cores: 0, + total_threads: 0, + frequency_mhz: 0, + speed: String::new(), + frequency_min_mhz: None, + frequency_max_mhz: None, + frequency_base_mhz: None, + architecture: std::env::consts::ARCH.to_string(), + microarchitecture: None, + cache_l1d_kb: None, + cache_l1i_kb: None, + cache_l2_kb: None, + cache_l3_kb: None, + caches: Vec::new(), + flags: Vec::new(), + microcode_version: None, + stepping: None, + family: None, + model_number: None, + virtualization: None, + numa_nodes: 1, + detection_methods: Vec::new(), + } + } +} + +impl CpuInfo { + /// Calculate total_cores and total_threads from topology. + pub fn calculate_totals(&mut self) { + self.total_cores = self.sockets * self.cores; + self.total_threads = self.total_cores * self.threads; + } + + /// Set legacy speed field from frequency_mhz. + pub fn set_speed_string(&mut self) { + if self.frequency_mhz > 0 { + if self.frequency_mhz >= 1000 { + self.speed = format!("{:.2} GHz", self.frequency_mhz as f64 / 1000.0); + } else { + self.speed = format!("{} MHz", self.frequency_mhz); + } + } + } +} +``` + +### 2.2 Add CPU Parser Functions + +**File:** `src/domain/parsers/cpu.rs` + +**LeetCode Pattern:** Parsing /proc/cpuinfo is a **State Machine** problem similar to +LC #65 (Valid Number) - we track state while processing each line. + +```rust +// ============================================================================= +// CPU PARSER FUNCTIONS +// ============================================================================= +// +// Architecture: DOMAIN layer - pure functions, no I/O +// ============================================================================= + +use crate::domain::{CpuInfo, CpuCacheInfo}; + +// ============================================================================= +// SYSFS FREQUENCY PARSING +// ============================================================================= +// +// LEETCODE CONNECTION: This is number parsing like LC #8 (atoi) +// but with unit conversion (kHz to MHz). +// ============================================================================= + +/// Parse sysfs frequency file (kHz) to MHz. +/// +/// The kernel reports CPU frequencies in kHz in sysfs. +/// +/// # Arguments +/// +/// * `content` - Content of cpufreq file (in kHz) +/// +/// # Example +/// +/// ```rust +/// use hardware_report::domain::parsers::cpu::parse_sysfs_freq_khz; +/// +/// // 3.5 GHz in kHz +/// assert_eq!(parse_sysfs_freq_khz("3500000").unwrap(), 3500); +/// assert_eq!(parse_sysfs_freq_khz("2100000\n").unwrap(), 2100); +/// ``` +pub fn parse_sysfs_freq_khz(content: &str) -> Result { + let khz: u32 = content + .trim() + .parse() + .map_err(|e| format!("Invalid frequency '{}': {}", content.trim(), e))?; + + // Convert kHz to MHz + Ok(khz / 1000) +} + +// ============================================================================= +// SYSFS CACHE SIZE PARSING +// ============================================================================= +// +// LEETCODE CONNECTION: Similar to LC #8 but with unit suffixes (K, M, G) +// Need to handle: "32K", "1M", "256K", "16M" +// ============================================================================= + +/// Parse sysfs cache size (e.g., "32K", "1M") to KB. +/// +/// # Arguments +/// +/// * `content` - Content of cache size file +/// +/// # Supported Units +/// +/// - K = kilobytes (multiply by 1) +/// - M = megabytes (multiply by 1024) +/// - G = gigabytes (multiply by 1024²) +/// +/// # Example +/// +/// ```rust +/// use hardware_report::domain::parsers::cpu::parse_sysfs_cache_size; +/// +/// assert_eq!(parse_sysfs_cache_size("32K").unwrap(), 32); +/// assert_eq!(parse_sysfs_cache_size("1M").unwrap(), 1024); +/// assert_eq!(parse_sysfs_cache_size("256K").unwrap(), 256); +/// ``` +pub fn parse_sysfs_cache_size(content: &str) -> Result { + let s = content.trim().to_uppercase(); + + // Handle common formats: "32K", "1M", "32768K" + if s.ends_with('K') { + let num_str = &s[..s.len()-1]; + num_str.parse::() + .map_err(|e| format!("Invalid cache size '{}': {}", s, e)) + } else if s.ends_with('M') { + let num_str = &s[..s.len()-1]; + num_str.parse::() + .map(|v| v * 1024) + .map_err(|e| format!("Invalid cache size '{}': {}", s, e)) + } else if s.ends_with('G') { + let num_str = &s[..s.len()-1]; + num_str.parse::() + .map(|v| v * 1024 * 1024) + .map_err(|e| format!("Invalid cache size '{}': {}", s, e)) + } else { + // Assume raw KB value + s.parse::() + .map_err(|e| format!("Invalid cache size '{}': {}", s, e)) + } +} + +// ============================================================================= +// /proc/cpuinfo PARSING +// ============================================================================= +// +// LEETCODE CONNECTION: This is a STATE MACHINE problem like: +// - LC #65 Valid Number +// - LC #10 Regular Expression Matching +// +// We process line by line, extracting key:value pairs. +// Different architectures (x86 vs ARM) have different keys! +// ============================================================================= + +/// Parse /proc/cpuinfo into CpuInfo. +/// +/// # Format Differences +/// +/// **x86/x86_64:** +/// ```text +/// model name : Intel(R) Xeon(R) Platinum 8380 CPU @ 2.30GHz +/// vendor_id : GenuineIntel +/// flags : fpu vme de pse avx avx2 avx512f ... +/// ``` +/// +/// **ARM/aarch64:** +/// ```text +/// CPU implementer : 0x41 +/// CPU part : 0xd0c +/// Features : fp asimd evtstrm aes ... +/// ``` +/// +/// # LeetCode Pattern +/// +/// This is similar to parsing problems: +/// - Split by delimiter (`:`) +/// - Handle whitespace +/// - Accumulate results +/// +/// # Example +/// +/// ```rust +/// use hardware_report::domain::parsers::cpu::parse_proc_cpuinfo; +/// +/// let cpuinfo = "model name\t: Intel Xeon\nflags\t: avx avx2\n"; +/// let info = parse_proc_cpuinfo(cpuinfo).unwrap(); +/// assert!(info.flags.contains(&"avx".to_string())); +/// ``` +pub fn parse_proc_cpuinfo(content: &str) -> Result { + let mut info = CpuInfo::default(); + let mut processor_count = 0; + + // Process each line + // PATTERN: Key-value parsing with colon delimiter + for line in content.lines() { + // Split on first colon + // "model name\t: Intel Xeon" -> ["model name\t", " Intel Xeon"] + let parts: Vec<&str> = line.splitn(2, ':').collect(); + + if parts.len() != 2 { + continue; + } + + let key = parts[0].trim().to_lowercase(); + let value = parts[1].trim(); + + // Match on key (different for x86 vs ARM) + match key.as_str() { + // x86 keys + "model name" => { + if info.model.is_empty() { + info.model = value.to_string(); + } + } + "vendor_id" => { + if info.vendor.is_empty() { + info.vendor = value.to_string(); + } + } + "cpu family" => { + info.family = value.parse().ok(); + } + "model" => { + // Note: "model" is the number, "model name" is the string + info.model_number = value.parse().ok(); + } + "stepping" => { + info.stepping = value.parse().ok(); + } + "microcode" => { + info.microcode_version = Some(value.to_string()); + } + "cpu mhz" => { + // Parse frequency from cpuinfo (may be floating point) + if let Ok(mhz) = value.parse::() { + info.frequency_mhz = mhz as u32; + } + } + "flags" => { + // x86 feature flags (space-separated) + info.flags = value.split_whitespace() + .map(String::from) + .collect(); + } + + // ARM keys + "features" => { + // ARM feature flags (like x86 "flags") + info.flags = value.split_whitespace() + .map(String::from) + .collect(); + } + "cpu implementer" => { + // ARM: indicates vendor + if info.vendor.is_empty() { + info.vendor = "ARM".to_string(); + } + } + "cpu part" => { + // ARM: CPU part number -> map to microarchitecture + if let Some(arch_name) = arm_cpu_part_to_name(value) { + info.microarchitecture = Some(arch_name.to_string()); + } + } + + // Count processors + "processor" => { + processor_count += 1; + } + + _ => {} + } + } + + // Set total_threads from processor count + if processor_count > 0 { + info.total_threads = processor_count; + } + + info.detection_methods.push("proc_cpuinfo".to_string()); + + Ok(info) +} + +// ============================================================================= +// ARM CPU PART MAPPING +// ============================================================================= +// +// LEETCODE CONNECTION: This is a HASH MAP lookup problem like: +// - LC #1 Two Sum (lookup in map) +// - LC #49 Group Anagrams (categorization) +// +// ARM CPUs are identified by a part number. We map to human-readable names. +// ============================================================================= + +/// Map ARM CPU part ID to microarchitecture name. +/// +/// ARM CPUs report a "CPU part" number in /proc/cpuinfo. +/// This function maps it to a human-readable name. +/// +/// # Arguments +/// +/// * `part` - CPU part from /proc/cpuinfo (e.g., "0xd0c") +/// +/// # Returns +/// +/// Human-readable microarchitecture name, or None if unknown. +/// +/// # Example +/// +/// ```rust +/// use hardware_report::domain::parsers::cpu::arm_cpu_part_to_name; +/// +/// assert_eq!(arm_cpu_part_to_name("0xd0c"), Some("Neoverse N1")); +/// assert_eq!(arm_cpu_part_to_name("0xd49"), Some("Neoverse N2")); +/// assert_eq!(arm_cpu_part_to_name("0xffff"), None); +/// ``` +/// +/// # References +/// +/// - [ARM CPU Part Numbers](https://developer.arm.com/documentation/ddi0487/latest) +/// - [Kernel ARM CPU table](https://github.com/torvalds/linux/blob/master/arch/arm64/kernel/cpuinfo.c) +pub fn arm_cpu_part_to_name(part: &str) -> Option<&'static str> { + // Normalize: remove "0x" prefix, convert to lowercase + let normalized = part.trim().to_lowercase(); + let part_id = normalized.strip_prefix("0x").unwrap_or(&normalized); + + // LEETCODE CONNECTION: This is essentially a hash map lookup + // In LeetCode terms: O(1) lookup after building the map + // We use match here for compile-time optimization + + match part_id { + // ARM Cortex-A series (mobile/embedded) + "d03" => Some("Cortex-A53"), + "d04" => Some("Cortex-A35"), + "d05" => Some("Cortex-A55"), + "d06" => Some("Cortex-A65"), + "d07" => Some("Cortex-A57"), + "d08" => Some("Cortex-A72"), + "d09" => Some("Cortex-A73"), + "d0a" => Some("Cortex-A75"), + "d0b" => Some("Cortex-A76"), + "d0c" => Some("Neoverse N1"), // Server (AWS Graviton2) + "d0d" => Some("Cortex-A77"), + "d0e" => Some("Cortex-A76AE"), + + // ARM Neoverse (server/cloud) + "d40" => Some("Neoverse V1"), // Server + "d41" => Some("Cortex-A78"), + "d42" => Some("Cortex-A78AE"), + "d43" => Some("Cortex-A65AE"), + "d44" => Some("Cortex-X1"), + "d46" => Some("Cortex-A510"), + "d47" => Some("Cortex-A710"), + "d48" => Some("Cortex-X2"), + "d49" => Some("Neoverse N2"), // Server (AWS Graviton3) + "d4a" => Some("Neoverse E1"), + "d4b" => Some("Cortex-A78C"), + "d4c" => Some("Cortex-X1C"), + "d4d" => Some("Cortex-A715"), + "d4e" => Some("Cortex-X3"), + "d4f" => Some("Neoverse V2"), // Server + + // Newer cores + "d80" => Some("Cortex-A520"), + "d81" => Some("Cortex-A720"), + "d82" => Some("Cortex-X4"), + + // NVIDIA (based on ARM) + "004" => Some("NVIDIA Denver"), + "003" => Some("NVIDIA Carmel"), + + _ => None, + } +} + +// ============================================================================= +// UNIT TESTS +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_sysfs_freq_khz() { + assert_eq!(parse_sysfs_freq_khz("3500000").unwrap(), 3500); + assert_eq!(parse_sysfs_freq_khz("2100000\n").unwrap(), 2100); + assert_eq!(parse_sysfs_freq_khz(" 1000000 ").unwrap(), 1000); + assert!(parse_sysfs_freq_khz("invalid").is_err()); + } + + #[test] + fn test_parse_sysfs_cache_size() { + assert_eq!(parse_sysfs_cache_size("32K").unwrap(), 32); + assert_eq!(parse_sysfs_cache_size("512K").unwrap(), 512); + assert_eq!(parse_sysfs_cache_size("1M").unwrap(), 1024); + assert_eq!(parse_sysfs_cache_size("32M").unwrap(), 32768); + assert_eq!(parse_sysfs_cache_size("32768K").unwrap(), 32768); + } + + #[test] + fn test_arm_cpu_part_mapping() { + assert_eq!(arm_cpu_part_to_name("0xd0c"), Some("Neoverse N1")); + assert_eq!(arm_cpu_part_to_name("0xd49"), Some("Neoverse N2")); + assert_eq!(arm_cpu_part_to_name("d0c"), Some("Neoverse N1")); // Without 0x + assert_eq!(arm_cpu_part_to_name("0xD0C"), Some("Neoverse N1")); // Uppercase + assert_eq!(arm_cpu_part_to_name("0xffff"), None); + } + + #[test] + fn test_parse_proc_cpuinfo_x86() { + let content = r#" +processor : 0 +vendor_id : GenuineIntel +cpu family : 6 +model : 106 +model name : Intel(R) Xeon(R) Platinum 8380 CPU @ 2.30GHz +stepping : 6 +microcode : 0xd0003a5 +cpu MHz : 2300.000 +flags : fpu vme avx avx2 avx512f +"#; + + let info = parse_proc_cpuinfo(content).unwrap(); + + assert_eq!(info.vendor, "GenuineIntel"); + assert!(info.model.contains("Xeon")); + assert_eq!(info.family, Some(6)); + assert_eq!(info.model_number, Some(106)); + assert_eq!(info.stepping, Some(6)); + assert!(info.flags.contains(&"avx512f".to_string())); + } + + #[test] + fn test_parse_proc_cpuinfo_arm() { + let content = r#" +processor : 0 +BogoMIPS : 50.00 +Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 +CPU implementer : 0x41 +CPU architecture: 8 +CPU variant : 0x3 +CPU part : 0xd0c +"#; + + let info = parse_proc_cpuinfo(content).unwrap(); + + assert_eq!(info.vendor, "ARM"); + assert_eq!(info.microarchitecture, Some("Neoverse N1".to_string())); + assert!(info.flags.contains(&"asimd".to_string())); + } +} +``` + +--- + +## Step 3: GPU Enhancements + +### 3.1 Add GpuVendor Enum + +**File:** `src/domain/entities.rs` + +**Where:** Add before the GpuDevice struct + +**LeetCode Pattern:** PCI vendor ID lookup is a **Hash Map** problem (LC #1 Two Sum). +We're mapping a key (vendor ID) to a value (vendor enum). + +```rust +// ============================================================================= +// GPU VENDOR ENUM +// ============================================================================= +// +// WHY: Different GPU vendors have different detection methods: +// - NVIDIA: NVML, nvidia-smi +// - AMD: ROCm, rocm-smi +// - Intel: sysfs +// +// LEETCODE CONNECTION: This is lookup/categorization like: +// - LC #1 Two Sum: lookup by key +// - LC #49 Group Anagrams: group by category +// ============================================================================= + +/// GPU vendor classification. +/// +/// Used to determine which detection method to use and +/// which vendor-specific features are available. +/// +/// # PCI Vendor IDs +/// +/// | Vendor | PCI ID | +/// |--------|--------| +/// | NVIDIA | 0x10de | +/// | AMD | 0x1002 | +/// | Intel | 0x8086 | +/// +/// # References +/// +/// - [PCI Vendor IDs](https://pci-ids.ucw.cz/) +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub enum GpuVendor { + /// NVIDIA Corporation (PCI vendor 0x10de). + /// + /// Detection: NVML, nvidia-smi + /// Features: CUDA, compute capability + Nvidia, + + /// Advanced Micro Devices (PCI vendor 0x1002). + /// + /// Detection: ROCm SMI, sysfs + /// Features: ROCm, HIP + Amd, + + /// Intel Corporation (PCI vendor 0x8086). + /// + /// Detection: sysfs, Intel GPU tools + /// Features: OpenCL, Level Zero + Intel, + + /// Apple Inc. (integrated GPUs on Apple Silicon). + /// + /// Detection: system_profiler + /// Features: Metal + Apple, + + /// Unknown or unrecognized vendor. + Unknown, +} + +impl Default for GpuVendor { + fn default() -> Self { + GpuVendor::Unknown + } +} + +impl GpuVendor { + /// Create GpuVendor from PCI vendor ID. + /// + /// # Arguments + /// + /// * `vendor_id` - PCI vendor ID (e.g., "10de", "0x10de") + /// + /// # Example + /// + /// ```rust + /// use hardware_report::GpuVendor; + /// + /// assert_eq!(GpuVendor::from_pci_vendor("10de"), GpuVendor::Nvidia); + /// assert_eq!(GpuVendor::from_pci_vendor("0x1002"), GpuVendor::Amd); + /// assert_eq!(GpuVendor::from_pci_vendor("8086"), GpuVendor::Intel); + /// ``` + /// + /// # LeetCode Pattern + /// + /// This is a simple hash lookup - O(1) time. + /// Similar to LC #1 Two Sum where you look up complement in a map. + pub fn from_pci_vendor(vendor_id: &str) -> Self { + // Normalize: remove "0x" prefix, convert to lowercase + let normalized = vendor_id.trim().to_lowercase(); + let id = normalized.strip_prefix("0x").unwrap_or(&normalized); + + match id { + "10de" => GpuVendor::Nvidia, + "1002" => GpuVendor::Amd, + "8086" => GpuVendor::Intel, + _ => GpuVendor::Unknown, + } + } + + /// Get the vendor name as string. + pub fn name(&self) -> &'static str { + match self { + GpuVendor::Nvidia => "NVIDIA", + GpuVendor::Amd => "AMD", + GpuVendor::Intel => "Intel", + GpuVendor::Apple => "Apple", + GpuVendor::Unknown => "Unknown", + } + } +} + +impl std::fmt::Display for GpuVendor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name()) + } +} +``` + +### 3.2 Update GpuDevice Struct + +**File:** `src/domain/entities.rs` + +**Where:** Replace the existing `GpuDevice` struct + +```rust +// ============================================================================= +// GPU DEVICE STRUCT +// ============================================================================= +// +// WHY THE CHANGES: +// OLD: memory: String ("80 GB") - can't parse! +// NEW: memory_total_mb: u64 (81920) - math works! +// +// DETECTION METHODS (Chain of Responsibility): +// 1. NVML (native library) - most accurate +// 2. nvidia-smi (command) - fallback for NVIDIA +// 3. rocm-smi (command) - AMD GPUs +// 4. sysfs /sys/class/drm - universal Linux +// 5. lspci - basic enumeration +// 6. sysinfo - cross-platform fallback +// ============================================================================= + +/// GPU device information. +/// +/// Represents a discrete or integrated GPU with comprehensive metadata. +/// +/// # Memory Format Change (v0.2.0) +/// +/// **BREAKING CHANGE**: Memory is now numeric! +/// +/// ```rust +/// // OLD (v0.1.x) - String that couldn't be parsed +/// let memory: &str = &gpu.memory; // "80 GB" +/// let mb: u64 = memory.parse().unwrap(); // FAILS! +/// +/// // NEW (v0.2.0) - Numeric, just works +/// let memory_mb: u64 = gpu.memory_total_mb; // 81920 +/// let memory_gb: f64 = memory_mb as f64 / 1024.0; // 80.0 +/// ``` +/// +/// # Detection Methods +/// +/// GPUs are detected using multiple methods: +/// +/// | Priority | Method | Vendor | Memory | Driver | +/// |----------|--------|--------|--------|--------| +/// | 1 | NVML | NVIDIA | Yes | Yes | +/// | 2 | nvidia-smi | NVIDIA | Yes | Yes | +/// | 3 | rocm-smi | AMD | Yes | Yes | +/// | 4 | sysfs DRM | All | Varies | No | +/// | 5 | lspci | All | No | No | +/// +/// # Example +/// +/// ```rust +/// use hardware_report::{GpuDevice, GpuVendor}; +/// +/// // Calculate total GPU memory across all GPUs +/// let gpus: Vec = get_gpus(); +/// let total_memory_gb: f64 = gpus.iter() +/// .map(|g| g.memory_total_mb as f64 / 1024.0) +/// .sum(); +/// +/// // Filter NVIDIA GPUs +/// let nvidia_gpus: Vec<_> = gpus.iter() +/// .filter(|g| g.vendor == GpuVendor::Nvidia) +/// .collect(); +/// ``` +/// +/// # References +/// +/// - [NVIDIA NVML](https://docs.nvidia.com/deploy/nvml-api/) +/// - [AMD ROCm SMI](https://rocm.docs.amd.com/projects/rocm_smi_lib/en/latest/) +/// - [Linux DRM](https://www.kernel.org/doc/html/latest/gpu/drm-uapi.html) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GpuDevice { + // ========================================================================= + // IDENTIFICATION + // ========================================================================= + + /// GPU index (0-based, unique per system). + pub index: u32, + + /// GPU product name. + /// + /// Examples: + /// - "NVIDIA H100 80GB HBM3" + /// - "AMD Instinct MI250X" + /// - "Intel Arc A770" + pub name: String, + + /// GPU UUID (globally unique identifier). + /// + /// NVIDIA format: "GPU-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + pub uuid: String, + + // ========================================================================= + // MEMORY (THE BIG FIX!) + // ========================================================================= + // + // LEETCODE CONNECTION: Having numeric types enables all the math: + // - LC #1 Two Sum: can now sum GPU memory + // - LC #215 Kth Largest: can sort by memory + // ========================================================================= + + /// Total GPU memory in megabytes. + /// + /// **PRIMARY FIELD** - use this for calculations! + /// + /// Examples: + /// - H100 80GB: 81920 MB + /// - A100 40GB: 40960 MB + /// - RTX 4090: 24576 MB + #[serde(default)] + pub memory_total_mb: u64, + + /// Free GPU memory in megabytes (runtime value). + /// + /// Returns None if not queryable (e.g., lspci-only detection). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub memory_free_mb: Option, + + /// Used GPU memory in megabytes. + /// + /// Calculated as: total - free + #[serde(default, skip_serializing_if = "Option::is_none")] + pub memory_used_mb: Option, + + /// Legacy memory as string (DEPRECATED). + /// + /// Kept for backward compatibility. Use `memory_total_mb` instead. + #[deprecated(since = "0.2.0", note = "Use memory_total_mb instead")] + pub memory: String, + + // ========================================================================= + // PCI INFORMATION + // ========================================================================= + + /// PCI vendor:device ID (e.g., "10de:2330"). + /// + /// Format: `{vendor_id}:{device_id}` in lowercase hex. + pub pci_id: String, + + /// PCI bus address (e.g., "0000:01:00.0"). + /// + /// Format: `{domain}:{bus}:{device}.{function}` + /// Useful for NUMA correlation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pci_bus_id: Option, + + // ========================================================================= + // VENDOR INFORMATION + // ========================================================================= + + /// GPU vendor enum. + /// + /// Use for programmatic comparisons. + #[serde(default)] + pub vendor: GpuVendor, + + /// Vendor name as string. + /// + /// For display and backward compatibility. + #[serde(default)] + pub vendor_name: String, + + // ========================================================================= + // DRIVER AND CAPABILITIES + // ========================================================================= + + /// GPU driver version. + /// + /// NVIDIA example: "535.129.03" + /// AMD example: "6.3.6" + #[serde(default, skip_serializing_if = "Option::is_none")] + pub driver_version: Option, + + /// CUDA compute capability (NVIDIA only). + /// + /// Format: "major.minor" + /// Examples: "9.0" (Hopper), "8.9" (Ada), "8.0" (Ampere) + /// + /// # References + /// + /// - [CUDA Compute Capability](https://developer.nvidia.com/cuda-gpus) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub compute_capability: Option, + + /// GPU architecture name. + /// + /// Examples: + /// - NVIDIA: "Hopper", "Ada Lovelace", "Ampere" + /// - AMD: "CDNA2", "RDNA3" + #[serde(default, skip_serializing_if = "Option::is_none")] + pub architecture: Option, + + // ========================================================================= + // TOPOLOGY + // ========================================================================= + + /// NUMA node affinity. + /// + /// Which NUMA node this GPU is attached to. + /// Important for optimal CPU-GPU data transfer. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub numa_node: Option, + + // ========================================================================= + // RUNTIME METRICS (Optional) + // ========================================================================= + + /// Current temperature in Celsius. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub temperature_celsius: Option, + + /// Power limit in watts. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub power_limit_watts: Option, + + /// Current power usage in watts. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub power_usage_watts: Option, + + /// GPU utilization percentage (0-100). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub utilization_percent: Option, + + // ========================================================================= + // METADATA + // ========================================================================= + + /// Detection method that discovered this GPU. + /// + /// Values: "nvml", "nvidia-smi", "rocm-smi", "sysfs", "lspci", "sysinfo" + #[serde(default)] + pub detection_method: String, +} + +impl Default for GpuDevice { + fn default() -> Self { + Self { + index: 0, + name: String::new(), + uuid: String::new(), + memory_total_mb: 0, + memory_free_mb: None, + memory_used_mb: None, + #[allow(deprecated)] + memory: String::new(), + pci_id: String::new(), + pci_bus_id: None, + vendor: GpuVendor::Unknown, + vendor_name: "Unknown".to_string(), + driver_version: None, + compute_capability: None, + architecture: None, + numa_node: None, + temperature_celsius: None, + power_limit_watts: None, + power_usage_watts: None, + utilization_percent: None, + detection_method: String::new(), + } + } +} + +impl GpuDevice { + /// Set the legacy memory string from memory_total_mb. + #[allow(deprecated)] + pub fn set_memory_string(&mut self) { + if self.memory_total_mb > 0 { + let gb = self.memory_total_mb as f64 / 1024.0; + if gb >= 1.0 { + self.memory = format!("{:.0} GB", gb); + } else { + self.memory = format!("{} MB", self.memory_total_mb); + } + } + } + + /// Calculate memory_used_mb from total and free. + pub fn calculate_memory_used(&mut self) { + if let Some(free) = self.memory_free_mb { + if self.memory_total_mb >= free { + self.memory_used_mb = Some(self.memory_total_mb - free); + } + } + } +} +``` + +### 3.3 Create GPU Parser Module + +**File:** `src/domain/parsers/gpu.rs` (NEW FILE) + +```rust +// ============================================================================= +// GPU PARSING MODULE +// ============================================================================= +// +// This module contains PURE FUNCTIONS for parsing GPU information +// from various sources. +// +// ARCHITECTURE: Domain layer - no I/O, no side effects +// ============================================================================= + +//! GPU information parsing functions. +//! +//! Pure parsing functions for GPU data from nvidia-smi, rocm-smi, lspci, etc. +//! +//! # Supported Formats +//! +//! - nvidia-smi CSV output +//! - rocm-smi JSON output +//! - lspci text output +//! +//! # References +//! +//! - [nvidia-smi](https://developer.nvidia.com/nvidia-system-management-interface) +//! - [rocm-smi](https://rocm.docs.amd.com/projects/rocm_smi_lib/en/latest/) + +use crate::domain::{GpuDevice, GpuVendor}; + +// ============================================================================= +// NVIDIA-SMI PARSING +// ============================================================================= +// +// LEETCODE CONNECTION: CSV parsing is like: +// - LC #722 Remove Comments: process structured text +// - LC #468 Validate IP Address: parse delimited fields +// +// Pattern: Split by delimiter, extract fields by position +// ============================================================================= + +/// Parse nvidia-smi CSV output into GPU devices. +/// +/// # Command +/// +/// ```bash +/// nvidia-smi --query-gpu=index,name,uuid,memory.total,memory.free,pci.bus_id,driver_version,compute_cap \ +/// --format=csv,noheader,nounits +/// ``` +/// +/// # Expected Format +/// +/// ```text +/// 0, NVIDIA H100 80GB HBM3, GPU-xxxx, 81920, 81000, 00000000:01:00.0, 535.129.03, 9.0 +/// 1, NVIDIA H100 80GB HBM3, GPU-yyyy, 81920, 80500, 00000000:02:00.0, 535.129.03, 9.0 +/// ``` +/// +/// # Fields +/// +/// 0. index +/// 1. name +/// 2. uuid +/// 3. memory.total (MiB, without units) +/// 4. memory.free (MiB) +/// 5. pci.bus_id +/// 6. driver_version +/// 7. compute_cap +/// +/// # Example +/// +/// ```rust +/// use hardware_report::domain::parsers::gpu::parse_nvidia_smi_output; +/// +/// let output = "0, NVIDIA H100, GPU-xxx, 81920, 81000, 00:01:00.0, 535.129.03, 9.0"; +/// let gpus = parse_nvidia_smi_output(output).unwrap(); +/// assert_eq!(gpus[0].memory_total_mb, 81920); +/// ``` +pub fn parse_nvidia_smi_output(output: &str) -> Result, String> { + let mut devices = Vec::new(); + + for line in output.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + + // Split by comma + // LEETCODE: This is like parsing CSV - split and extract + let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect(); + + // Need at least 4 fields (index, name, uuid, memory) + if parts.len() < 4 { + continue; + } + + // Parse index + let index: u32 = parts[0].parse().unwrap_or(devices.len() as u32); + + // Parse memory (nvidia-smi with nounits gives MiB directly) + let memory_total_mb: u64 = parts.get(3) + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + + let memory_free_mb: Option = parts.get(4) + .and_then(|s| s.parse().ok()); + + let mut device = GpuDevice { + index, + name: parts.get(1).unwrap_or(&"").to_string(), + uuid: parts.get(2).unwrap_or(&"").to_string(), + memory_total_mb, + memory_free_mb, + pci_bus_id: parts.get(5).map(|s| s.to_string()), + driver_version: parts.get(6).map(|s| s.to_string()), + compute_capability: parts.get(7).map(|s| s.to_string()), + vendor: GpuVendor::Nvidia, + vendor_name: "NVIDIA".to_string(), + detection_method: "nvidia-smi".to_string(), + ..Default::default() + }; + + // Set legacy fields + device.set_memory_string(); + device.calculate_memory_used(); + + // Build PCI ID from bus ID if possible + // Bus ID format: 00000000:01:00.0 + // We'd need device ID from somewhere else for full pci_id + + devices.push(device); + } + + Ok(devices) +} + +// ============================================================================= +// LSPCI PARSING +// ============================================================================= +// +// LEETCODE CONNECTION: This is pattern matching in strings: +// - LC #28 Find Index of First Occurrence +// - LC #10 Regular Expression Matching +// +// We scan for GPU-related PCI class codes +// ============================================================================= + +/// Parse lspci output for GPU devices. +/// +/// # Command +/// +/// ```bash +/// lspci -nn +/// ``` +/// +/// # Expected Format +/// +/// ```text +/// 01:00.0 3D controller [0302]: NVIDIA Corporation GH100 [H100] [10de:2330] (rev a1) +/// 02:00.0 VGA compatible controller [0300]: Advanced Micro Devices [1002:73bf] +/// ``` +/// +/// # PCI Class Codes +/// +/// - 0300: VGA compatible controller +/// - 0302: 3D controller (NVIDIA compute GPUs) +/// - 0380: Display controller +/// +/// # Limitations +/// +/// lspci does NOT provide: +/// - GPU memory (returns 0) +/// - Driver version +/// - UUID +/// +/// Use this as fallback enumeration only. +pub fn parse_lspci_gpu_output(output: &str) -> Result, String> { + let mut devices = Vec::new(); + let mut gpu_index = 0; + + for line in output.lines() { + let line_lower = line.to_lowercase(); + + // Check for GPU-related PCI classes + // [0300] = VGA controller + // [0302] = 3D controller + // [0380] = Display controller + let is_gpu = line_lower.contains("[0300]") + || line_lower.contains("[0302]") + || line_lower.contains("[0380]") + || line_lower.contains("vga compatible") + || line_lower.contains("3d controller") + || line_lower.contains("display controller"); + + if !is_gpu { + continue; + } + + // Extract PCI bus ID (first field) + // Format: "01:00.0 3D controller..." + let pci_bus_id = line.split_whitespace().next().map(String::from); + + // Extract vendor:device ID + // Look for pattern [xxxx:yyyy] + let pci_id = extract_pci_id(line); + + // Determine vendor from PCI ID + let vendor = pci_id.as_ref() + .map(|id| { + let vendor_id = id.split(':').next().unwrap_or(""); + GpuVendor::from_pci_vendor(vendor_id) + }) + .unwrap_or(GpuVendor::Unknown); + + // Extract name (everything between class and PCI ID) + let name = extract_gpu_name_from_lspci(line); + + let device = GpuDevice { + index: gpu_index, + name, + uuid: format!("pci-{}", pci_bus_id.as_deref().unwrap_or("unknown")), + pci_id: pci_id.unwrap_or_default(), + pci_bus_id, + vendor: vendor.clone(), + vendor_name: vendor.name().to_string(), + detection_method: "lspci".to_string(), + // NOTE: lspci cannot determine memory! + memory_total_mb: 0, + ..Default::default() + }; + + devices.push(device); + gpu_index += 1; + } + + Ok(devices) +} + +/// Extract PCI vendor:device ID from lspci line. +/// +/// Looks for pattern `[xxxx:yyyy]` at end of line. +fn extract_pci_id(line: &str) -> Option { + // Find the last occurrence of [xxxx:yyyy] + // LEETCODE: This is like LC #28 - finding a pattern + + let mut result = None; + let mut remaining = line; + + while let Some(start) = remaining.find('[') { + if let Some(end) = remaining[start..].find(']') { + let bracket_content = &remaining[start+1..start+end]; + + // Check if it looks like a PCI ID (xxxx:yyyy) + if bracket_content.len() == 9 && bracket_content.chars().nth(4) == Some(':') { + // Verify it's hex + let parts: Vec<&str> = bracket_content.split(':').collect(); + if parts.len() == 2 + && parts[0].chars().all(|c| c.is_ascii_hexdigit()) + && parts[1].chars().all(|c| c.is_ascii_hexdigit()) + { + result = Some(bracket_content.to_lowercase()); + } + } + + remaining = &remaining[start+end+1..]; + } else { + break; + } + } + + result +} + +/// Extract GPU name from lspci line. +fn extract_gpu_name_from_lspci(line: &str) -> String { + // Try to find the name between the class description and PCI IDs + // "01:00.0 3D controller [0302]: NVIDIA Corporation GH100 [H100] [10de:2330]" + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + if let Some(colon_pos) = line.find("]:") { + let after_class = &line[colon_pos + 2..].trim(); + + // Find where PCI IDs start (last [xxxx:yyyy]) + if let Some(pci_start) = after_class.rfind('[') { + let name = after_class[..pci_start].trim(); + // Remove trailing [device name] brackets too + if let Some(name_end) = name.rfind('[') { + return name[..name_end].trim().to_string(); + } + return name.to_string(); + } + return after_class.to_string(); + } + + line.to_string() +} + +// ============================================================================= +// PCI VENDOR LOOKUP +// ============================================================================= + +/// Parse PCI vendor ID to GpuVendor. +/// +/// # Arguments +/// +/// * `vendor_id` - Hex string (e.g., "10de", "0x10de") +/// +/// # Example +/// +/// ```rust +/// use hardware_report::domain::parsers::gpu::parse_pci_vendor; +/// use hardware_report::GpuVendor; +/// +/// assert_eq!(parse_pci_vendor("10de"), GpuVendor::Nvidia); +/// assert_eq!(parse_pci_vendor("0x1002"), GpuVendor::Amd); +/// ``` +pub fn parse_pci_vendor(vendor_id: &str) -> GpuVendor { + GpuVendor::from_pci_vendor(vendor_id) +} + +// ============================================================================= +// UNIT TESTS +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_nvidia_smi_output() { + let output = r#"0, NVIDIA H100 80GB HBM3, GPU-12345678-1234-1234-1234-123456789abc, 81920, 81000, 00000000:01:00.0, 535.129.03, 9.0 +1, NVIDIA H100 80GB HBM3, GPU-87654321-4321-4321-4321-cba987654321, 81920, 80500, 00000000:02:00.0, 535.129.03, 9.0"#; + + let gpus = parse_nvidia_smi_output(output).unwrap(); + + assert_eq!(gpus.len(), 2); + assert_eq!(gpus[0].index, 0); + assert_eq!(gpus[0].name, "NVIDIA H100 80GB HBM3"); + assert_eq!(gpus[0].memory_total_mb, 81920); + assert_eq!(gpus[0].memory_free_mb, Some(81000)); + assert_eq!(gpus[0].driver_version, Some("535.129.03".to_string())); + assert_eq!(gpus[0].compute_capability, Some("9.0".to_string())); + assert_eq!(gpus[0].vendor, GpuVendor::Nvidia); + } + + #[test] + fn test_parse_nvidia_smi_empty() { + let output = ""; + let gpus = parse_nvidia_smi_output(output).unwrap(); + assert!(gpus.is_empty()); + } + + #[test] + fn test_parse_lspci_gpu_output() { + let output = r#" +00:02.0 VGA compatible controller [0300]: Intel Corporation Device [8086:9a49] (rev 01) +01:00.0 3D controller [0302]: NVIDIA Corporation GH100 [H100 SXM5 80GB] [10de:2330] (rev a1) +02:00.0 3D controller [0302]: NVIDIA Corporation GH100 [H100 SXM5 80GB] [10de:2330] (rev a1) +"#; + + let gpus = parse_lspci_gpu_output(output).unwrap(); + + assert_eq!(gpus.len(), 3); + + // Intel GPU + assert_eq!(gpus[0].vendor, GpuVendor::Intel); + assert_eq!(gpus[0].pci_id, "8086:9a49"); + + // NVIDIA GPUs + assert_eq!(gpus[1].vendor, GpuVendor::Nvidia); + assert_eq!(gpus[1].pci_id, "10de:2330"); + assert_eq!(gpus[1].pci_bus_id, Some("01:00.0".to_string())); + } + + #[test] + fn test_extract_pci_id() { + assert_eq!( + extract_pci_id("...controller [0302]: NVIDIA [10de:2330] (rev a1)"), + Some("10de:2330".to_string()) + ); + assert_eq!( + extract_pci_id("...controller [0300]: Intel [8086:9a49]"), + Some("8086:9a49".to_string()) + ); + assert_eq!(extract_pci_id("no pci id here"), None); + } + + #[test] + fn test_parse_pci_vendor() { + assert_eq!(parse_pci_vendor("10de"), GpuVendor::Nvidia); + assert_eq!(parse_pci_vendor("0x10de"), GpuVendor::Nvidia); + assert_eq!(parse_pci_vendor("1002"), GpuVendor::Amd); + assert_eq!(parse_pci_vendor("8086"), GpuVendor::Intel); + assert_eq!(parse_pci_vendor("unknown"), GpuVendor::Unknown); + } +} +``` + +--- + +## Step 4: Memory Enhancements + +### 4.1 Update MemoryModule Struct + +**File:** `src/domain/entities.rs` + +**Where:** Update the existing `MemoryModule` struct + +```rust +// ============================================================================= +// MEMORY MODULE STRUCT +// ============================================================================= +// +// KEY ADDITION: part_number field for asset tracking! +// +// CMDB use case: "Which exact memory do we need to order for replacement?" +// Answer: Look up the part_number and order that exact module. +// ============================================================================= + +/// Memory technology type. +/// +/// # References +/// +/// - [JEDEC Standards](https://www.jedec.org/) +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] +pub enum MemoryType { + Ddr3, + Ddr4, + Ddr5, + Lpddr4, + Lpddr5, + Hbm2, + Hbm3, + #[default] + Unknown, +} + +impl MemoryType { + /// Parse memory type from string. + pub fn from_string(type_str: &str) -> Self { + match type_str.to_uppercase().as_str() { + "DDR3" => MemoryType::Ddr3, + "DDR4" => MemoryType::Ddr4, + "DDR5" => MemoryType::Ddr5, + "LPDDR4" | "LPDDR4X" => MemoryType::Lpddr4, + "LPDDR5" | "LPDDR5X" => MemoryType::Lpddr5, + "HBM2" | "HBM2E" => MemoryType::Hbm2, + "HBM3" | "HBM3E" => MemoryType::Hbm3, + _ => MemoryType::Unknown, + } + } +} + +/// Individual memory module (DIMM). +/// +/// # New Fields (v0.2.0) +/// +/// - `part_number` - Manufacturer part number for ordering +/// - `size_bytes` - Numeric size for calculations +/// - `speed_mhz` - Numeric speed for comparisons +/// +/// # Example +/// +/// ```rust +/// use hardware_report::MemoryModule; +/// +/// // Calculate total memory +/// let total_gb: f64 = modules.iter() +/// .map(|m| m.size_bytes as f64 / (1024.0 * 1024.0 * 1024.0)) +/// .sum(); +/// +/// // Find part number for ordering replacement +/// let part = modules[0].part_number.as_deref().unwrap_or("Unknown"); +/// println!("Order part: {}", part); +/// ``` +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MemoryModule { + /// Physical slot location (e.g., "DIMM_A1", "ChannelA-DIMM0"). + pub location: String, + + /// Bank locator (e.g., "BANK 0", "P0 CHANNEL A"). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bank_locator: Option, + + /// Module size in bytes. + /// + /// PRIMARY SIZE FIELD - use for calculations. + #[serde(default)] + pub size_bytes: u64, + + /// Module size as string (e.g., "32 GB"). + /// + /// For display and backward compatibility. + pub size: String, + + /// Memory type enum. + #[serde(default)] + pub memory_type: MemoryType, + + /// Memory type as string (e.g., "DDR4", "DDR5"). + #[serde(rename = "type")] + pub type_: String, + + /// Speed in MT/s (megatransfers per second). + /// + /// DDR4-3200 = 3200 MT/s + #[serde(default, skip_serializing_if = "Option::is_none")] + pub speed_mts: Option, + + /// Configured clock speed in MHz. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub speed_mhz: Option, + + /// Speed as string (e.g., "3200 MT/s"). + pub speed: String, + + /// Manufacturer name (e.g., "Samsung", "Micron", "SK Hynix"). + pub manufacturer: String, + + /// Module serial number. + pub serial: String, + + /// Manufacturer part number. + /// + /// **IMPORTANT** for procurement and warranty! + /// + /// Examples: + /// - "M393A4K40EB3-CWE" (Samsung 32GB DDR4-3200) + /// - "MTA36ASF8G72PZ-3G2E1" (Micron 64GB DDR4-3200) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub part_number: Option, + + /// Number of memory ranks (1, 2, 4, or 8). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub rank: Option, + + /// Data width in bits (64, 72 for ECC). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub data_width_bits: Option, + + /// Whether ECC is supported. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ecc: Option, + + /// Configured voltage in volts. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub voltage: Option, +} + +impl Default for MemoryModule { + fn default() -> Self { + Self { + location: String::new(), + bank_locator: None, + size_bytes: 0, + size: String::new(), + memory_type: MemoryType::Unknown, + type_: String::new(), + speed_mts: None, + speed_mhz: None, + speed: String::new(), + manufacturer: String::new(), + serial: String::new(), + part_number: None, + rank: None, + data_width_bits: None, + ecc: None, + voltage: None, + } + } +} +``` + +--- + +## Step 5: Network Enhancements + +### 5.1 Update NetworkInterface Struct + +**File:** `src/domain/entities.rs` + +```rust +// ============================================================================= +// NETWORK INTERFACE TYPE ENUM +// ============================================================================= + +/// Network interface type classification. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] +pub enum NetworkInterfaceType { + Ethernet, + Wireless, + Loopback, + Bridge, + Vlan, + Bond, + Veth, + TunTap, + Infiniband, + #[default] + Unknown, +} + +/// Network interface information. +/// +/// # New Fields (v0.2.0) +/// +/// - `driver` / `driver_version` - For compatibility tracking +/// - `speed_mbps` - Numeric speed +/// - `mtu` - Maximum transmission unit +/// - `is_up` / `is_virtual` - State flags +/// +/// # Example +/// +/// ```rust +/// // Find all 10G+ physical interfaces that are up +/// let fast_nics: Vec<_> = interfaces.iter() +/// .filter(|i| i.is_up && !i.is_virtual && i.speed_mbps.unwrap_or(0) >= 10000) +/// .collect(); +/// ``` +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct NetworkInterface { + /// Interface name (e.g., "eth0", "ens192"). + pub name: String, + + /// MAC address (e.g., "00:11:22:33:44:55"). + pub mac: String, + + /// Primary IPv4 address. + pub ip: String, + + /// Network prefix length (e.g., "24"). + pub prefix: String, + + /// Link speed in Mbps. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub speed_mbps: Option, + + /// Link speed as string. + pub speed: Option, + + /// Interface type enum. + #[serde(default)] + pub interface_type: NetworkInterfaceType, + + /// Interface type as string. + #[serde(rename = "type")] + pub type_: String, + + /// Hardware vendor name. + pub vendor: String, + + /// Hardware model. + pub model: String, + + /// PCI vendor:device ID. + pub pci_id: String, + + /// NUMA node affinity. + pub numa_node: Option, + + /// Kernel driver in use (e.g., "igb", "mlx5_core"). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub driver: Option, + + /// Driver version. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub driver_version: Option, + + /// Firmware version. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub firmware_version: Option, + + /// Maximum Transmission Unit in bytes. + #[serde(default)] + pub mtu: u32, + + /// Whether interface is operationally up. + #[serde(default)] + pub is_up: bool, + + /// Whether this is a virtual interface. + #[serde(default)] + pub is_virtual: bool, + + /// Link detected (carrier present). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub carrier: Option, + + /// Duplex mode ("full", "half"). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub duplex: Option, +} + +impl Default for NetworkInterface { + fn default() -> Self { + Self { + name: String::new(), + mac: String::new(), + ip: String::new(), + prefix: String::new(), + speed_mbps: None, + speed: None, + interface_type: NetworkInterfaceType::Unknown, + type_: String::new(), + vendor: String::new(), + model: String::new(), + pci_id: String::new(), + numa_node: None, + driver: None, + driver_version: None, + firmware_version: None, + mtu: 1500, + is_up: false, + is_virtual: false, + carrier: None, + duplex: None, + } + } +} +``` + +--- + +## Step 6: Update Cargo.toml + +**File:** `Cargo.toml` + +Add feature flags for optional dependencies: + +```toml +[package] +name = "hardware_report" +version = "0.2.0" # Bump version for breaking changes +edition = "2021" +authors = ["Kenny Sheridan"] +description = "A tool for generating hardware information reports" + +[dependencies] +# Existing dependencies... +lazy_static = "1.4" +tonic = "0.10" +reqwest = { version = "0.11", features = ["json"] } +structopt = "0.3" +sysinfo = "0.32.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +clap = { version = "4.4", features = ["derive"] } +thiserror = "1.0" +log = "0.4" +env_logger = "0.11.5" +regex = "1.11.1" +toml = "0.8.19" +libc = "0.2.161" +tokio = { version = "1.0", features = ["full"] } +async-trait = "0.1" + +# NEW: Optional NVIDIA GPU support via NVML +# Requires NVIDIA driver at runtime +nvml-wrapper = { version = "0.9", optional = true } + +# x86-specific CPU detection (only on x86/x86_64) +[target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies] +raw-cpuid = { version = "11", optional = true } + +[features] +default = [] +nvidia = ["nvml-wrapper"] +x86-cpu = ["raw-cpuid"] +full = ["nvidia", "x86-cpu"] + +[dev-dependencies] +tempfile = "3.8" +assert_fs = "1.0" +predicates = "3.0" + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +strip = true + +[lib] +name = "hardware_report" +path = "src/lib.rs" + +[[bin]] +name = "hardware_report" +path = "src/bin/hardware_report.rs" +``` + +--- + +## Summary: Implementation Checklist + +Use this checklist as you implement: + +### Entities (`src/domain/entities.rs`) + +- [ ] Add `StorageType` enum (after line 205) +- [ ] Update `StorageDevice` struct with new fields +- [ ] Add `Default` impl for `StorageType` +- [ ] Add `Default` impl for `StorageDevice` +- [ ] Add `CpuCacheInfo` struct +- [ ] Update `CpuInfo` struct with new fields +- [ ] Add `GpuVendor` enum +- [ ] Update `GpuDevice` struct with numeric memory +- [ ] Update `MemoryModule` with `part_number` +- [ ] Add `NetworkInterfaceType` enum +- [ ] Update `NetworkInterface` with driver fields + +### Parsers (`src/domain/parsers/`) + +- [ ] Update `storage.rs` with new functions +- [ ] Update `cpu.rs` with sysfs/cpuinfo parsing +- [ ] Create `gpu.rs` module (new file) +- [ ] Update `mod.rs` to export `gpu` + +### Adapters (`src/adapters/secondary/system/linux.rs`) + +- [ ] Update `get_storage_info` with sysfs detection +- [ ] Update `get_cpu_info` with frequency/cache +- [ ] Update `get_gpu_info` with multi-method detection +- [ ] Add helper methods for sysfs reading + +### Configuration + +- [ ] Update `Cargo.toml` with features +- [ ] Update version to `0.2.0` + +### Testing + +- [ ] Run `cargo check` after each change +- [ ] Run `cargo test` to verify parsers +- [ ] Test on ARM hardware if available + +--- + +## LeetCode Practice Problems + +To solidify your understanding of the patterns used, practice these: + +| Pattern | LeetCode Problem | Used In | +|---------|------------------|---------| +| Hash Map Lookup | LC #1 Two Sum | PCI vendor ID → vendor name | +| String Parsing | LC #8 atoi | sysfs frequency parsing | +| String Parsing | LC #468 Validate IP Address | MAC address validation | +| Tree Traversal | LC #102 Level Order | Cache hierarchy, sysfs directory walking | +| Merge | LC #88 Merge Sorted Array | Merging GPU info from multiple sources | +| Filter | LC #283 Move Zeroes | Filter virtual devices | +| Categorization | LC #49 Group Anagrams | StorageType, GpuVendor classification | +| State Machine | LC #65 Valid Number | Parsing multi-line /proc/cpuinfo | +| Pattern Matching | LC #28 Find Index | Extract PCI ID from lspci | + +Good luck with your implementation! Type it in yourself to learn the patterns. + + + +[{"id": "learn-1", "content": "Update ENHANCEMENTS.md with implementation guide overview", "status": "completed", "priority": "high"}, {"id": "learn-2", "content": "Update STORAGE_DETECTION.md with full implementation code", "status": "completed", "priority": "high"}, {"id": "learn-3", "content": "Update GPU_DETECTION.md with full implementation code", "status": "in_progress", "priority": "high"}, {"id": "learn-4", "content": "Update CPU_ENHANCEMENTS.md with full implementation code", "status": "pending", "priority": "high"}, {"id": "learn-5", "content": "Update MEMORY_ENHANCEMENTS.md with full implementation code", "status": "pending", "priority": "medium"}, {"id": "learn-6", "content": "Update NETWORK_ENHANCEMENTS.md with full implementation code", "status": "pending", "priority": "medium"}, {"id": "learn-7", "content": "Create step-by-step implementation checklist", "status": "pending", "priority": "high"}] \ No newline at end of file diff --git a/src/domain/entities.rs b/src/domain/entities.rs index b292382..2f16784 100644 --- a/src/domain/entities.rs +++ b/src/domain/entities.rs @@ -211,45 +211,67 @@ pub struct StorageInfo { pub devices: Vec, } -/// Storage device information -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct StorageDevice { - /// Device name - pub name: String, - /// Device type (e.g., ssd, hdd) - pub type_: String, - /// Device size - pub size: String, - /// Device model - pub model: String, -} -/// GPU information -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct GpuInfo { - /// List of GPU devices - pub devices: Vec, -} /// GPU device information -#[derive(Debug, Serialize, Deserialize, Clone)] +/// +/// Represents a discrete or integrated GPU detected in the system. +/// Memory values are provided in megabytes as unsigned integers for +/// reliable parsing by CMDB consumers. +/// +/// # Detection Methods +/// +/// GPUs are detected using multiple methods in priority order: +/// 1. NVML (NVIDIA Management Library) - most accurate for NVIDIA GPUs +/// 2. nvidia-smi command - fallback for NVIDIA when NVML unavailable +/// 3. ROCm SMI - AMD GPU detection +/// 4. sysfs /sys/class/drm - Linux DRM subsystem +/// 5. lspci - PCI device enumeration +/// 6. sysinfo crate - cross-platform fallback +/// +/// # References +/// +/// - [NVIDIA NVML Documentation](https://developer.nvidia.com/nvidia-management-library-nvml) +/// - [Linux DRM Subsystem](https://www.kernel.org/doc/html/latest/gpu/drm-uapi.html) +/// - [PCI ID Database](https://pci-ids.ucw.cz/) +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct GpuDevice { - /// GPU index + /// GPU index (0-based) pub index: u32, - /// GPU name + + /// GPU product name pub name: String, - /// GPU UUID + + /// GPU UUUD pub uuid: String, - /// Total GPU memory - pub memory: String, - /// PCI ID (vendor:device) or Apple Fabric for Apple Silicon - pub pci_id: String, + /// Vendor name pub vendor: String, - /// NUMA node + + /// Driver Version + pub driver_version: Option, + + /// CUDA compute capability for Nvidia gpus + pub compute_capability: Option, + + /// GPU architecturr (Hopper, Ada LoveLace) + pub architecture: Option, + + /// NUMA node affiniity (-1 if not applicable) pub numa_node: Option, + + /// Detection method used to dsicover this GPU + pub detection_method: String, } + + +/// GPU information +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GpuInfo { + /// List of GPU devices + pub devices: Vec, +} /// Network information #[derive(Debug, Serialize, Deserialize, Clone)] pub struct NetworkInfo { @@ -259,6 +281,30 @@ pub struct NetworkInfo { pub infiniband: Option, } +/// Storage device type classification +/// +/// # References +/// +/// - [Linux Block Device Documentation](https://www.kernel.org/doc/html/latest/block/index.html) +/// - [NVMe Specification](https://nvmexpress.org/specifications/) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum StorageType { + /// NVMe ssd + Nvme, + + /// SATA/SAS ssd + Ssd, + + /// Hard disk (rotational) + Hdd, + + /// Embedded MMC Storage + Emmc, + + /// Unknown or unclassified storage type + Unknown, +} + /// Network interface information #[derive(Debug, Serialize, Deserialize, Clone)] pub struct NetworkInterface { @@ -284,6 +330,67 @@ pub struct NetworkInterface { pub numa_node: Option, } +/// Storage device information +/// +/// # Detection Methods +/// +/// Storage devices are detected using multiple methods in priority order: +/// 1. sysfs /sys/block - direct kernel interface (Linux) +/// 2. lsblk command - block device listing +/// 3. sysinfo crate - cross-platform fallback +/// 4. diskutil (macOS) +/// +/// # References +/// +/// - [Linux sysfs Block Devices](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-block) +/// - [SMART Attributes](https://en.wikipedia.org/wiki/S.M.A.R.T.) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct StorageDevice { + /// Device name (nvme0n1, sda etc..,) + pub name: String, + + /// Device type classification + pub device_type: StorageType, + + /// Legacy type field + #[deprecated(since = "0.2.0", note = "Use device_type instead")] + #[serde(skip_serializing_if = "Option::is_none")] + pub type_: Option, + + /// Device size in bytes + pub size_bytes: u64, + + /// Device size in gigabyes + pub size_gb: f64, + + /// Legacy size field as string (deprecated) + #[deprecated(since = "0.2.0", note = "Use size_bytes or size_gb instead")] + #[serde(skip_serializing_if = "Option::is_none")] + pub size: Option, + + /// Device model name + pub model: String, + + /// Device serial number (may require elevated privileges) + pub serial_number: Option, + + /// Device firmware version + pub firmware_version: Option, + + /// Interface type (e.g., "NVMe", "SATA", "SAS", "eMMC") + pub interface: String, + + /// Whether the device is rotational (true = HDD, false = SSD/NVMe) + pub is_rotational: bool, + + /// WWN (World Wide Name) if available + pub wwn: Option, + + /// Detection method used + pub detection_method: String, + +} + /// Infiniband information #[derive(Debug, Serialize, Deserialize, Clone)] pub struct InfinibandInfo { From 2fae4767658c0101c950701c97a78a8b7a3baac3 Mon Sep 17 00:00:00 2001 From: "Kenny (Knight) Sheridan" Date: Tue, 30 Dec 2025 15:27:18 -0800 Subject: [PATCH 3/8] added NetworkInterface --- src/domain/entities.rs | 83 +++++++++++++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 22 deletions(-) diff --git a/src/domain/entities.rs b/src/domain/entities.rs index 2f16784..cc32abf 100644 --- a/src/domain/entities.rs +++ b/src/domain/entities.rs @@ -159,21 +159,6 @@ pub struct HardwareInfo { pub gpus: GpuInfo, } -/// CPU information -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct CpuInfo { - /// CPU model name - pub model: String, - /// Number of cores per socket - pub cores: u32, - /// Number of threads per core - pub threads: u32, - /// Number of sockets - pub sockets: u32, - /// CPU speed - pub speed: String, -} - /// Memory information #[derive(Debug, Serialize, Deserialize, Clone)] pub struct MemoryInfo { @@ -202,6 +187,9 @@ pub struct MemoryModule { pub manufacturer: String, /// Serial number pub serial: String, + pub part_number: Option, + pub rank: Option, + pub configured_voltage: Option, } /// Storage information @@ -211,8 +199,6 @@ pub struct StorageInfo { pub devices: Vec, } - - /// GPU device information /// /// Represents a discrete or integrated GPU detected in the system. @@ -264,8 +250,6 @@ pub struct GpuDevice { pub detection_method: String, } - - /// GPU information #[derive(Debug, Serialize, Deserialize, Clone)] pub struct GpuInfo { @@ -301,7 +285,7 @@ pub enum StorageType { /// Embedded MMC Storage Emmc, - /// Unknown or unclassified storage type + /// Unknown or unclassified storage type Unknown, } @@ -328,6 +312,12 @@ pub struct NetworkInterface { pub pci_id: String, /// NUMA node pub numa_node: Option, + pub driver: Option, + pub driver_version: Option, + pub firmware_version: Option, + pub mtu: u32, + pub is_up: bool, + pub is_virtual: bool, } /// Storage device information @@ -352,7 +342,7 @@ pub struct StorageDevice { /// Device type classification pub device_type: StorageType, - /// Legacy type field + /// Legacy type field #[deprecated(since = "0.2.0", note = "Use device_type instead")] #[serde(skip_serializing_if = "Option::is_none")] pub type_: Option, @@ -360,7 +350,7 @@ pub struct StorageDevice { /// Device size in bytes pub size_bytes: u64, - /// Device size in gigabyes + /// Device size in gigabyes pub size_gb: f64, /// Legacy size field as string (deprecated) @@ -388,7 +378,56 @@ pub struct StorageDevice { /// Detection method used pub detection_method: String, +} + +/// # Detection Methods +/// +/// CPU information is gathered from multiple sources: +/// 1. sysfs /sys/devices/system/cpu - frequency and cache (Linux) +/// 2. /proc/cpuinfo - model and features (Linux) +/// 3. raw-cpuid crate - x86 CPUID instruction +/// 4. lscpu command - topology information +/// 5. dmidecode - SMBIOS data (requires privileges) +/// 6. sysinfo crate - cross-platform fallback +/// +/// # References +/// +/// - [Linux CPU sysfs Interface](https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.rst) +/// - [Intel CPUID Reference](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) +/// - [ARM CPU Identification](https://developer.arm.com/documentation/ddi0487/latest) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CpuInfo { + /// Cpu Model (AMD, Intel) + pub model: String, + + /// CPu Vendor + pub vendor: String, + + /// Number of phyiscal cores per socket + pub cores: u32, + + /// Number of threads per core + pub threads: u32, + + /// Number of CPU sockets + pub sockets: u32, + + /// CPU frequencies in MHz (uccrent or max) + pub frequency_mhz: u32, + + /// Legacy speed field as string (deprecated) + #[deprecated(since = "0.2.0", note = "Use frequency_mhz instead")] + #[serde(skip_serializing_if = "Option::is_none")] + pub speed: Option, + + /// CPU architecture + pub arhitecture: String, + + /// LI data cache size in kilobytes (per core) + pub cache_l1d_kb: Option, + /// L1 instruction cache size in kilobytes (per core) + pub cache_l1li_kb: Option, } /// Infiniband information From 8925b01ac25edb4cb2c5a5c87b968e528a284711 Mon Sep 17 00:00:00 2001 From: "Kenny (Knight) Sheridan" Date: Tue, 30 Dec 2025 15:31:11 -0800 Subject: [PATCH 4/8] added nvml-wrapper --- Cargo.lock | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 14 ++++++++ 2 files changed, 113 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 814c3c7..5189894 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -367,6 +367,41 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.101", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.101", +] + [[package]] name = "difflib" version = "0.4.0" @@ -611,6 +646,7 @@ dependencies = [ "lazy_static", "libc", "log", + "nvml-wrapper", "predicates", "regex", "reqwest", @@ -830,6 +866,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -951,6 +993,16 @@ version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -1058,6 +1110,29 @@ dependencies = [ "autocfg", ] +[[package]] +name = "nvml-wrapper" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d5c6c0ef9702176a570f06ad94f3198bc29c524c8b498f1b9346e1b1bdcbb3a" +dependencies = [ + "bitflags 2.9.1", + "libloading", + "nvml-wrapper-sys", + "static_assertions", + "thiserror", + "wrapcenum-derive", +] + +[[package]] +name = "nvml-wrapper-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd23dbe2eb8d8335d2bce0299e0a07d6a63c089243d626ca75b770a962ff49e6" +dependencies = [ + "libloading", +] + [[package]] name = "object" version = "0.36.7" @@ -1624,6 +1699,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.8.0" @@ -2251,6 +2332,12 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-result" version = "0.1.2" @@ -2436,6 +2523,18 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "wrapcenum-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76ff259533532054cfbaefb115c613203c73707017459206380f03b3b3f266e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "writeable" version = "0.6.1" diff --git a/Cargo.toml b/Cargo.toml index e37bb16..87d0a06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,20 @@ clap = { version = "4.4", features = [ "derive", ] } # For command line argument parsing thiserror = "1.0" # For error handling +# Requires Nvidia driver at runtime +nvml-wrapper = { version = "0.11.0", optional = true} + +# Only compiled on x86/x86_64 targets +[target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies] +raw-cpuid = { version = "11", optional = true } + +[features] +default = [] +nvidia = ["nvml-wrapper"] +x86-cpu = ["raw-cpuid"] +full = ["nvidia", "x86-cpu"] + +# Requires Nvidia driver at runtime log = "0.4" # For logging env_logger = "0.11.5" # For logging implementation regex = "1.11.1" From 27c95aa4a6be3f741606999b3d9c45bcdcbdd19d Mon Sep 17 00:00:00 2001 From: "Kenny (Knight) Sheridan" Date: Mon, 5 Jan 2026 16:29:38 -0800 Subject: [PATCH 5/8] added enhancements to hardware report for Netbox Ingestify --- Cargo.lock | 10 + Cargo.toml | 16 +- MYQQGPTJ6J_hardware_report.json | 58 +- MYQQGPTJ6J_hardware_report.toml | 55 +- docs/LINUX_ADAPTER_IMPLEMENTATION.md | 1822 ++++++++++++++++++++++ src/adapters/secondary/publisher/file.rs | 1 + src/adapters/secondary/publisher/http.rs | 1 + src/adapters/secondary/system/linux.rs | 469 +++++- src/adapters/secondary/system/macos.rs | 2 + src/domain/entities.rs | 624 +++++--- src/domain/errors.rs | 23 +- src/domain/legacy_compat.rs | 6 +- src/domain/parsers/cpu.rs | 100 ++ src/domain/parsers/gpu.rs | 219 +++ src/domain/parsers/mod.rs | 2 + src/domain/parsers/network.rs | 2 + src/domain/parsers/storage.rs | 147 +- 17 files changed, 3299 insertions(+), 258 deletions(-) create mode 100644 docs/LINUX_ADAPTER_IMPLEMENTATION.md create mode 100644 src/domain/parsers/gpu.rs diff --git a/Cargo.lock b/Cargo.lock index 5189894..b5b464a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -648,6 +648,7 @@ dependencies = [ "log", "nvml-wrapper", "predicates", + "raw-cpuid", "regex", "reqwest", "serde", @@ -1415,6 +1416,15 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.9.1", +] + [[package]] name = "rayon" version = "1.10.0" diff --git a/Cargo.toml b/Cargo.toml index 87d0a06..021c8ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,13 @@ clap = { version = "4.4", features = [ "derive", ] } # For command line argument parsing thiserror = "1.0" # For error handling +log = "0.4" # For logging +env_logger = "0.11.5" # For logging implementation +regex = "1.11.1" +toml = "0.8.19" +libc = "0.2.161" +tokio = { version = "1.0", features = ["full"] } +async-trait = "0.1" # Requires Nvidia driver at runtime nvml-wrapper = { version = "0.11.0", optional = true} @@ -32,15 +39,6 @@ nvidia = ["nvml-wrapper"] x86-cpu = ["raw-cpuid"] full = ["nvidia", "x86-cpu"] -# Requires Nvidia driver at runtime -log = "0.4" # For logging -env_logger = "0.11.5" # For logging implementation -regex = "1.11.1" -toml = "0.8.19" -libc = "0.2.161" -tokio = { version = "1.0", features = ["full"] } -async-trait = "0.1" - [dev-dependencies] tempfile = "3.8" # For temporary file handling in tests assert_fs = "1.0" # For filesystem assertions in tests diff --git a/MYQQGPTJ6J_hardware_report.json b/MYQQGPTJ6J_hardware_report.json index 8d3c417..9455d12 100644 --- a/MYQQGPTJ6J_hardware_report.json +++ b/MYQQGPTJ6J_hardware_report.json @@ -11,20 +11,20 @@ "total_storage": "2.0 TB", "total_storage_tb": 1.81464035063982, "filesystems": [ - "/dev/disk3s1s1 (apfs) - 1.9T total, 262G used, 1.6T available, mounted on /", - "/dev/disk3s6 (apfs) - 1.9T total, 262G used, 1.6T available, mounted on /System/Volumes/VM", - "/dev/disk3s2 (apfs) - 1.9T total, 262G used, 1.6T available, mounted on /System/Volumes/Preboot", - "/dev/disk3s4 (apfs) - 1.9T total, 262G used, 1.6T available, mounted on /System/Volumes/Update", - "/dev/disk1s2 (apfs) - 500M total, 19M used, 482M available, mounted on /System/Volumes/xarts", - "/dev/disk1s1 (apfs) - 500M total, 19M used, 482M available, mounted on /System/Volumes/iSCPreboot", - "/dev/disk1s3 (apfs) - 500M total, 19M used, 482M available, mounted on /System/Volumes/Hardware", - "/dev/disk3s7 (apfs) - 1.9T total, 262G used, 1.6T available, mounted on /nix" + "/dev/disk3s1s1 (apfs) - 1.9T total, 1.3T used, 615G available, mounted on /", + "/dev/disk3s6 (apfs) - 1.9T total, 1.3T used, 615G available, mounted on /System/Volumes/VM", + "/dev/disk3s2 (apfs) - 1.9T total, 1.3T used, 615G available, mounted on /System/Volumes/Preboot", + "/dev/disk3s4 (apfs) - 1.9T total, 1.3T used, 615G available, mounted on /System/Volumes/Update", + "/dev/disk1s2 (apfs) - 500M total, 20M used, 481M available, mounted on /System/Volumes/xarts", + "/dev/disk1s1 (apfs) - 500M total, 20M used, 481M available, mounted on /System/Volumes/iSCPreboot", + "/dev/disk1s3 (apfs) - 500M total, 20M used, 481M available, mounted on /System/Volumes/Hardware", + "/dev/disk3s7 (apfs) - 1.9T total, 1.3T used, 615G available, mounted on /nix" ], "bios": { "vendor": "Apple Inc.", - "version": "11881.121.1", + "version": "13822.41.1", "release_date": "N/A", - "firmware_version": "11881.121.1" + "firmware_version": "13822.41.1" }, "chassis": { "manufacturer": "Apple Inc.", @@ -34,14 +34,14 @@ "motherboard": { "manufacturer": "Apple Inc.", "product_name": "Mac16,5", - "version": "11881.121.1", + "version": "13822.41.1", "serial": "MYQQGPTJ6J", "features": "Integrated", "location": "System Board", "type_": "Motherboard" }, "total_gpus": 1, - "total_nics": 6, + "total_nics": 8, "numa_topology": {}, "cpu_topology": { "total_cores": 16, @@ -96,7 +96,7 @@ "devices": [ { "index": 0, - "name": "Apple M4 Max (Metal 3)", + "name": "Apple M4 Max (Metal 4)", "uuid": "macOS-GPU-0", "memory": "Unified Memory (40 cores)", "pci_id": "Apple Fabric (Integrated)", @@ -112,6 +112,7 @@ "name": "en4", "mac": "56", "ip": "Unknown", + "prefix": "Unknown", "speed": "1000 Mbps", "type_": "Ethernet", "vendor": "Apple", @@ -123,6 +124,7 @@ "name": "en5", "mac": "56", "ip": "Unknown", + "prefix": "Unknown", "speed": "1000 Mbps", "type_": "Ethernet", "vendor": "Apple", @@ -134,6 +136,7 @@ "name": "en6", "mac": "56", "ip": "Unknown", + "prefix": "Unknown", "speed": "1000 Mbps", "type_": "Ethernet", "vendor": "Apple", @@ -145,6 +148,7 @@ "name": "bridge0", "mac": "Unknown", "ip": "Unknown", + "prefix": "Unknown", "speed": "1000 Mbps", "type_": "Ethernet", "vendor": "Apple", @@ -156,6 +160,7 @@ "name": "en0", "mac": "84", "ip": "192.168.1.90", + "prefix": "Unknown", "speed": "1200 Mbps", "type_": "AirPort", "vendor": "Apple", @@ -163,10 +168,35 @@ "pci_id": "Apple Fabric (Integrated)", "numa_node": null }, + { + "name": "en7", + "mac": "Unknown", + "ip": "Unknown", + "prefix": "Unknown", + "speed": "1000 Mbps", + "type_": "Ethernet", + "vendor": "Apple", + "model": "Ethernet", + "pci_id": "Apple Fabric (Integrated)", + "numa_node": null + }, { "name": "utun4", "mac": "Unknown", - "ip": "100.84.190.125", + "ip": "100.112.156.46", + "prefix": "Unknown", + "speed": null, + "type_": "VPN (io.tailscale.ipn.macsys)", + "vendor": "Unknown", + "model": "Unknown", + "pci_id": "Unknown", + "numa_node": null + }, + { + "name": "Tailscale 3", + "mac": "Unknown", + "ip": "Unknown", + "prefix": "Unknown", "speed": null, "type_": "VPN (io.tailscale.ipn.macos)", "vendor": "Unknown", diff --git a/MYQQGPTJ6J_hardware_report.toml b/MYQQGPTJ6J_hardware_report.toml index 64d2904..92716b0 100644 --- a/MYQQGPTJ6J_hardware_report.toml +++ b/MYQQGPTJ6J_hardware_report.toml @@ -8,17 +8,17 @@ memory_config = "LPDDR5 @ Integrated" total_storage = "2.0 TB" total_storage_tb = 1.81464035063982 filesystems = [ - "/dev/disk3s1s1 (apfs) - 1.9T total, 262G used, 1.6T available, mounted on /", - "/dev/disk3s6 (apfs) - 1.9T total, 262G used, 1.6T available, mounted on /System/Volumes/VM", - "/dev/disk3s2 (apfs) - 1.9T total, 262G used, 1.6T available, mounted on /System/Volumes/Preboot", - "/dev/disk3s4 (apfs) - 1.9T total, 262G used, 1.6T available, mounted on /System/Volumes/Update", - "/dev/disk1s2 (apfs) - 500M total, 19M used, 482M available, mounted on /System/Volumes/xarts", - "/dev/disk1s1 (apfs) - 500M total, 19M used, 482M available, mounted on /System/Volumes/iSCPreboot", - "/dev/disk1s3 (apfs) - 500M total, 19M used, 482M available, mounted on /System/Volumes/Hardware", - "/dev/disk3s7 (apfs) - 1.9T total, 262G used, 1.6T available, mounted on /nix", + "/dev/disk3s1s1 (apfs) - 1.9T total, 1.3T used, 615G available, mounted on /", + "/dev/disk3s6 (apfs) - 1.9T total, 1.3T used, 615G available, mounted on /System/Volumes/VM", + "/dev/disk3s2 (apfs) - 1.9T total, 1.3T used, 615G available, mounted on /System/Volumes/Preboot", + "/dev/disk3s4 (apfs) - 1.9T total, 1.3T used, 615G available, mounted on /System/Volumes/Update", + "/dev/disk1s2 (apfs) - 500M total, 20M used, 481M available, mounted on /System/Volumes/xarts", + "/dev/disk1s1 (apfs) - 500M total, 20M used, 481M available, mounted on /System/Volumes/iSCPreboot", + "/dev/disk1s3 (apfs) - 500M total, 20M used, 481M available, mounted on /System/Volumes/Hardware", + "/dev/disk3s7 (apfs) - 1.9T total, 1.3T used, 615G available, mounted on /nix", ] total_gpus = 1 -total_nics = 6 +total_nics = 8 cpu_summary = "Apple M4 Max (1 Socket, 16 Cores/Socket, 1 Thread/Core, 1 NUMA Node)" [summary.system_info] @@ -29,9 +29,9 @@ product_manufacturer = "Apple Inc." [summary.bios] vendor = "Apple Inc." -version = "11881.121.1" +version = "13822.41.1" release_date = "N/A" -firmware_version = "11881.121.1" +firmware_version = "13822.41.1" [summary.chassis] manufacturer = "Apple Inc." @@ -41,7 +41,7 @@ serial = "MYQQGPTJ6J" [summary.motherboard] manufacturer = "Apple Inc." product_name = "Mac16,5" -version = "11881.121.1" +version = "13822.41.1" serial = "MYQQGPTJ6J" features = "Integrated" location = "System Board" @@ -86,7 +86,7 @@ model = "APPLE SSD AP2048Z (Apple Fabric)" [[hardware.gpus.devices]] index = 0 -name = "Apple M4 Max (Metal 3)" +name = "Apple M4 Max (Metal 4)" uuid = "macOS-GPU-0" memory = "Unified Memory (40 cores)" pci_id = "Apple Fabric (Integrated)" @@ -96,6 +96,7 @@ vendor = "Apple" name = "en4" mac = "56" ip = "Unknown" +prefix = "Unknown" speed = "1000 Mbps" type_ = "Ethernet" vendor = "Apple" @@ -106,6 +107,7 @@ pci_id = "Apple Fabric (Integrated)" name = "en5" mac = "56" ip = "Unknown" +prefix = "Unknown" speed = "1000 Mbps" type_ = "Ethernet" vendor = "Apple" @@ -116,6 +118,7 @@ pci_id = "Apple Fabric (Integrated)" name = "en6" mac = "56" ip = "Unknown" +prefix = "Unknown" speed = "1000 Mbps" type_ = "Ethernet" vendor = "Apple" @@ -126,6 +129,7 @@ pci_id = "Apple Fabric (Integrated)" name = "bridge0" mac = "Unknown" ip = "Unknown" +prefix = "Unknown" speed = "1000 Mbps" type_ = "Ethernet" vendor = "Apple" @@ -136,16 +140,39 @@ pci_id = "Apple Fabric (Integrated)" name = "en0" mac = "84" ip = "192.168.1.90" +prefix = "Unknown" speed = "1200 Mbps" type_ = "AirPort" vendor = "Apple" model = "Wi-Fi 802.11 a/b/g/n/ac/ax" pci_id = "Apple Fabric (Integrated)" +[[network.interfaces]] +name = "en7" +mac = "Unknown" +ip = "Unknown" +prefix = "Unknown" +speed = "1000 Mbps" +type_ = "Ethernet" +vendor = "Apple" +model = "Ethernet" +pci_id = "Apple Fabric (Integrated)" + [[network.interfaces]] name = "utun4" mac = "Unknown" -ip = "100.84.190.125" +ip = "100.112.156.46" +prefix = "Unknown" +type_ = "VPN (io.tailscale.ipn.macsys)" +vendor = "Unknown" +model = "Unknown" +pci_id = "Unknown" + +[[network.interfaces]] +name = "Tailscale 3" +mac = "Unknown" +ip = "Unknown" +prefix = "Unknown" type_ = "VPN (io.tailscale.ipn.macos)" vendor = "Unknown" model = "Unknown" diff --git a/docs/LINUX_ADAPTER_IMPLEMENTATION.md b/docs/LINUX_ADAPTER_IMPLEMENTATION.md new file mode 100644 index 0000000..4279629 --- /dev/null +++ b/docs/LINUX_ADAPTER_IMPLEMENTATION.md @@ -0,0 +1,1822 @@ +# Linux Adapter Implementation Guide + +> **File:** `src/adapters/secondary/system/linux.rs` +> **Purpose:** Platform-specific hardware detection for Linux (x86_64 and aarch64) +> **Architecture:** Adapter layer in Hexagonal/Ports-and-Adapters pattern + +## Table of Contents + +1. [Overview](#overview) +2. [Architecture Context](#architecture-context) +3. [Prerequisites](#prerequisites) +4. [Implementation Steps](#implementation-steps) + - [Step 1: Update Imports](#step-1-update-imports) + - [Step 2: Storage Detection](#step-2-storage-detection) + - [Step 3: CPU Detection](#step-3-cpu-detection) + - [Step 4: GPU Detection](#step-4-gpu-detection) + - [Step 5: Network Detection](#step-5-network-detection) +5. [Helper Functions](#helper-functions) +6. [Testing](#testing) +7. [LeetCode Pattern Summary](#leetcode-pattern-summary) + +--- + +## Overview + +The `LinuxSystemInfoProvider` is an **adapter** that implements the `SystemInfoProvider` **port** (trait). It translates abstract hardware queries into Linux-specific operations (sysfs reads, command execution). + +### What Changes? + +| Method | Current | New | +|--------|---------|-----| +| `get_storage_info` | lsblk only | sysfs primary + lsblk enrichment + sysinfo fallback | +| `get_cpu_info` | lscpu + dmidecode | + sysfs for frequency/cache | +| `get_gpu_info` | nvidia-smi + lspci | + multi-method chain with numeric memory | +| `get_network_info` | ip command | + sysfs for driver/MTU/state | + +--- + +## Architecture Context + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ YOUR CODE CHANGES │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ ADAPTER LAYER: src/adapters/secondary/system/linux.rs │ +│ │ +│ LinuxSystemInfoProvider │ +│ ├── get_storage_info() ← MODIFY: Add sysfs detection │ +│ ├── get_cpu_info() ← MODIFY: Add frequency/cache │ +│ ├── get_gpu_info() ← MODIFY: Multi-method + numeric memory │ +│ └── get_network_info() ← MODIFY: Add driver/MTU │ +│ │ +│ Helper methods (NEW): │ +│ ├── detect_storage_sysfs() │ +│ ├── detect_storage_lsblk() │ +│ ├── detect_cpu_sysfs_frequency() │ +│ ├── detect_cpu_sysfs_cache() │ +│ ├── detect_gpus_nvidia_smi() │ +│ ├── detect_gpus_lspci() │ +│ ├── detect_gpus_sysfs_drm() │ +│ ├── detect_network_sysfs() │ +│ ├── read_sysfs_file() │ +│ └── merge_*_info() │ +└─────────────────────────────────────────────────────────────────────┘ + │ + │ implements + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ PORT LAYER: src/ports/secondary/system.rs │ +│ │ +│ trait SystemInfoProvider { │ +│ fn get_storage_info() -> Result │ +│ fn get_cpu_info() -> Result │ +│ fn get_gpu_info() -> Result │ +│ fn get_network_info() -> Result │ +│ } │ +└─────────────────────────────────────────────────────────────────────┘ + │ + │ uses + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ DOMAIN LAYER: src/domain/ │ +│ │ +│ entities.rs - StorageDevice, CpuInfo, GpuDevice, etc. │ +│ parsers/ - Pure parsing functions (no I/O) │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Prerequisites + +Before modifying `linux.rs`, ensure you have: + +1. **Updated `entities.rs`** with: + - `StorageType` enum + - Updated `StorageDevice` struct + - Updated `CpuInfo` struct with cache/frequency + - `GpuVendor` enum + - Updated `GpuDevice` struct with numeric memory + - Updated `NetworkInterface` struct + +2. **Updated `parsers/storage.rs`** with: + - `parse_sysfs_size()` + - `parse_sysfs_rotational()` + - `parse_lsblk_json()` + - `is_virtual_device()` + +3. **Created `parsers/gpu.rs`** with: + - `parse_nvidia_smi_output()` + - `parse_lspci_gpu_output()` + +4. **Updated `parsers/cpu.rs`** with: + - `parse_sysfs_freq_khz()` + - `parse_sysfs_cache_size()` + - `parse_proc_cpuinfo()` + +--- + +## Implementation Steps + +### Step 1: Update Imports + +**Location:** Top of `linux.rs` (lines 17-30) + +**Replace the existing imports with:** + +```rust +// ============================================================================= +// IMPORTS +// ============================================================================= +// +// ARCHITECTURE NOTE: +// - We import from `domain` (entities and parsers) +// - We import from `ports` (the trait we implement) +// - We DO NOT import from other adapters (adapters are independent) +// +// LEETCODE CONNECTION: Dependency management is like LC #210 Course Schedule II +// - There's an ordering: domain → ports → adapters +// - Circular dependencies would break the build +// ============================================================================= + +//! Linux system information provider +//! +//! Implements `SystemInfoProvider` for Linux systems using: +//! - sysfs (`/sys`) for direct kernel data +//! - procfs (`/proc`) for process/system info +//! - Command execution for tools like lsblk, nvidia-smi +//! +//! # Platform Support +//! +//! - x86_64: Full support including raw-cpuid +//! - aarch64: Full support via sysfs (ARM servers, DGX Spark) +//! +//! # Detection Strategy +//! +//! Each hardware type uses multiple detection methods: +//! 1. Primary: sysfs (most reliable, always available) +//! 2. Secondary: Command output (lsblk, nvidia-smi, etc.) +//! 3. Fallback: sysinfo crate (cross-platform) + +use crate::domain::{ + // Existing imports - keep these + combine_cpu_info, determine_memory_speed, determine_memory_type, + parse_dmidecode_bios_info, parse_dmidecode_chassis_info, parse_dmidecode_cpu, + parse_dmidecode_memory, parse_dmidecode_system_info, parse_free_output, + parse_hostname_output, parse_ip_output, parse_lscpu_output, + BiosInfo, ChassisInfo, MemoryInfo, MotherboardInfo, NumaNode, SystemInfo, + + // NEW imports for enhanced entities + CpuInfo, CpuCacheInfo, GpuInfo, GpuDevice, GpuVendor, + StorageInfo, StorageDevice, StorageType, + NetworkInfo, NetworkInterface, NetworkInterfaceType, + + // NEW imports for parsers + SystemError, +}; + +// NEW: Import parser functions +use crate::domain::parsers::storage::{ + parse_sysfs_size, parse_sysfs_rotational, parse_lsblk_json, is_virtual_device, +}; +use crate::domain::parsers::cpu::{ + parse_sysfs_freq_khz, parse_sysfs_cache_size, parse_proc_cpuinfo, +}; +use crate::domain::parsers::gpu::{ + parse_nvidia_smi_output, parse_lspci_gpu_output, +}; + +use crate::ports::{CommandExecutor, SystemCommand, SystemInfoProvider}; +use async_trait::async_trait; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; + +// NEW: Standard library imports for sysfs reading +use std::fs; +use std::path::{Path, PathBuf}; +``` + +--- + +### Step 2: Storage Detection + +**Location:** Replace `get_storage_info` method (around line 153-170) + +**LeetCode Patterns:** +- **Chain of Responsibility**: Try sysfs → lsblk → sysinfo +- **Merge/Combine** (LC #88): Combine results from multiple sources +- **Tree Traversal** (LC #102): Walk /sys/block directory + +```rust +// ============================================================================= +// STORAGE DETECTION +// ============================================================================= +// +// PROBLEM SOLVED: +// - Old code used only lsblk, which fails on some ARM platforms +// - New code uses sysfs as primary (works everywhere on Linux) +// +// DETECTION CHAIN (Chain of Responsibility pattern): +// 1. sysfs /sys/block - Primary, most reliable +// 2. lsblk -J - Enrichment (WWN, transport type) +// 3. sysinfo crate - Fallback if above fail +// +// LEETCODE CONNECTION: +// - LC #88 Merge Sorted Array: we merge info from multiple sources +// - LC #200 Number of Islands: walking the sysfs "grid" +// ============================================================================= + +async fn get_storage_info(&self) -> Result { + // ========================================================================= + // STEP 1: Primary detection via sysfs + // ========================================================================= + // + // WHY SYSFS FIRST? + // - Direct kernel interface - always available on Linux + // - No external tools required (lsblk might not be installed) + // - Works identically on x86_64 and aarch64 + // - Doesn't require parsing command output (more reliable) + // + // sysfs structure: + // /sys/block/ + // ├── sda/ + // │ ├── size # Size in 512-byte sectors + // │ ├── queue/ + // │ │ └── rotational # 0=SSD, 1=HDD + // │ └── device/ + // │ ├── model # Device model + // │ └── serial # Serial number (may need root) + // ├── nvme0n1/ + // └── mmcblk0/ # eMMC on ARM + // ========================================================================= + + let mut devices = Vec::new(); + + // if let chaining - cleaner than match for "try or log warning" + if let Ok(sysfs_devices) = self.detect_storage_sysfs().await { + log::debug!("sysfs detected {} storage devices", sysfs_devices.len()); + devices = sysfs_devices; + } else { + log::warn!("sysfs storage detection failed, trying next method"); + } + + // ========================================================================= + // STEP 2: Enrichment via lsblk + // ========================================================================= + // + // Even if sysfs worked, lsblk may have additional data: + // - WWN (World Wide Name) + // - Transport type (nvme, sata, usb) + // - Serial (sometimes easier to get via lsblk) + // + // MERGE STRATEGY: + // - Match by device name + // - Fill in missing fields from lsblk + // - Don't overwrite existing data (sysfs is more reliable) + // + // LEETCODE CONNECTION: This is the merge pattern + // Similar to LC #88 Merge Sorted Array, but merging by key (device name) + // ========================================================================= + + if let Ok(lsblk_devices) = self.detect_storage_lsblk().await { + log::debug!( + "lsblk found {} devices for enrichment", + lsblk_devices.len() + ); + self.merge_storage_info(&mut devices, lsblk_devices); + } + + // ========================================================================= + // STEP 3: Fallback via sysinfo crate + // ========================================================================= + // + // If we still have no devices, something unusual is happening. + // Try sysinfo as a cross-platform fallback. + // + // This can happen in: + // - Containers with limited /sys access + // - Unusual system configurations + // ========================================================================= + + if devices.is_empty() { + log::warn!("No devices from sysfs/lsblk, trying sysinfo fallback"); + if let Ok(sysinfo_devices) = self.detect_storage_sysinfo() { + devices = sysinfo_devices; + } + } + + // ========================================================================= + // POST-PROCESSING + // ========================================================================= + // + // 1. Filter virtual devices (loop, ram, dm-*) + // 2. Ensure size fields are calculated + // 3. Sort for consistent output + // + // LEETCODE CONNECTION: + // - Filtering is like LC #283 Move Zeroes (filter in-place) + // - Sorting is standard LC pattern + // ========================================================================= + + // Filter out virtual devices - they're not physical hardware + // PATTERN: retain() is more efficient than filter() + collect() + devices.retain(|d| d.device_type != StorageType::Virtual); + + // Ensure all calculated fields are populated + for device in &mut devices { + if device.size_gb == 0.0 && device.size_bytes > 0 { + device.calculate_size_fields(); + } + device.set_device_path(); + } + + // Sort by name for consistent, predictable output + devices.sort_by(|a, b| a.name.cmp(&b.name)); + + log::info!("Detected {} storage devices", devices.len()); + Ok(StorageInfo { devices }) +} +``` + +**Add these helper methods to `impl LinuxSystemInfoProvider`:** + +```rust +// ============================================================================= +// STORAGE HELPER METHODS +// ============================================================================= + +impl LinuxSystemInfoProvider { + /// Detect storage devices via sysfs /sys/block. + /// + /// # How It Works + /// + /// 1. Read directory listing of /sys/block + /// 2. For each device, read attributes from sysfs files + /// 3. Build StorageDevice struct + /// + /// # sysfs Paths Used + /// + /// | Path | Content | Example | + /// |------|---------|---------| + /// | `/sys/block/{dev}/size` | Sectors (×512=bytes) | "3907029168" | + /// | `/sys/block/{dev}/queue/rotational` | 0=SSD, 1=HDD | "0" | + /// | `/sys/block/{dev}/device/model` | Model name | "Samsung SSD 980" | + /// | `/sys/block/{dev}/device/serial` | Serial (may need root) | "S5GXNF0N1234" | + /// + /// # LeetCode Connection + /// + /// This is **directory traversal** similar to: + /// - LC #200 Number of Islands (grid traversal) + /// - LC #130 Surrounded Regions + /// - LC #417 Pacific Atlantic Water Flow + /// + /// We're walking a tree structure (filesystem) and extracting data. + async fn detect_storage_sysfs(&self) -> Result, SystemError> { + let mut devices = Vec::new(); + + // Path to block devices in sysfs + let sys_block = Path::new("/sys/block"); + + // Check if sysfs is mounted/accessible + if !sys_block.exists() { + return Err(SystemError::NotAvailable { + resource: "/sys/block".to_string(), + }); + } + + // Read directory entries + // PATTERN: This is the "traversal" part - we visit each node (device) + let entries = fs::read_dir(sys_block).map_err(|e| { + SystemError::IoError { + path: "/sys/block".to_string(), + message: e.to_string(), + } + })?; + + // Process each block device + for entry in entries.flatten() { + let device_name = entry.file_name().to_string_lossy().to_string(); + + // ───────────────────────────────────────────────────────────── + // EARLY FILTERING: Skip virtual devices + // ───────────────────────────────────────────────────────────── + // WHY EARLY? Saves I/O - don't read attributes for devices we'll skip + // PATTERN: This is like LC #283 Move Zeroes - filter early + if is_virtual_device(&device_name) { + log::trace!("Skipping virtual device: {}", device_name); + continue; + } + + let device_path = entry.path(); + + // ───────────────────────────────────────────────────────────── + // READ SIZE (required field) + // ───────────────────────────────────────────────────────────── + // If we can't get size, skip this device (probably not real storage) + // + // PATTERN: let-else for early return/continue on failure + // This is cleaner than nested match statements + let size_path = device_path.join("size"); + let Ok(content) = self.read_sysfs_file(&size_path) else { + continue; + }; + let Ok(size_bytes) = parse_sysfs_size(&content) else { + log::trace!("Cannot parse size for {}: invalid format", device_name); + continue; + }; + + // Skip tiny devices (< 1GB) - probably not real storage + // USB sticks, boot partitions, etc. + const MIN_SIZE: u64 = 1_000_000_000; // 1 GB + if size_bytes < MIN_SIZE { + log::trace!("Skipping small device {}: {} bytes", device_name, size_bytes); + continue; + } + + // ───────────────────────────────────────────────────────────── + // READ ROTATIONAL FLAG + // ───────────────────────────────────────────────────────────── + // 0 = SSD/NVMe (no spinning platters) + // 1 = HDD (spinning platters) + let rotational_path = device_path.join("queue/rotational"); + let is_rotational = self.read_sysfs_file(&rotational_path) + .map(|content| parse_sysfs_rotational(&content)) + .unwrap_or(false); // Default to SSD if unknown + + // ───────────────────────────────────────────────────────────── + // DETERMINE DEVICE TYPE + // ───────────────────────────────────────────────────────────── + // Combines name pattern + rotational flag + let device_type = StorageType::from_device(&device_name, is_rotational); + + // ───────────────────────────────────────────────────────────── + // READ OPTIONAL FIELDS + // ───────────────────────────────────────────────────────────── + // These may fail (especially serial without root) - that's OK + + // Model name + let model = self.read_sysfs_file(&device_path.join("device/model")) + .map(|s| s.trim().to_string()) + .unwrap_or_default(); + + // Serial number (may require root) + let serial_number = self.read_sysfs_file(&device_path.join("device/serial")) + .map(|s| s.trim().to_string()) + .ok() + .filter(|s| !s.is_empty()); + + // Firmware version + let firmware_version = self.read_sysfs_file(&device_path.join("device/firmware_rev")) + .map(|s| s.trim().to_string()) + .ok() + .filter(|s| !s.is_empty()); + + // For NVMe, try alternate paths + let (serial_number, firmware_version) = if device_type == StorageType::Nvme { + self.read_nvme_sysfs_attrs(&device_name, serial_number, firmware_version) + } else { + (serial_number, firmware_version) + }; + + // ───────────────────────────────────────────────────────────── + // DETERMINE INTERFACE TYPE + // ───────────────────────────────────────────────────────────── + let interface = match &device_type { + StorageType::Nvme => "NVMe".to_string(), + StorageType::Emmc => "eMMC".to_string(), + StorageType::Hdd | StorageType::Ssd => "SATA".to_string(), + _ => "Unknown".to_string(), + }; + + // ───────────────────────────────────────────────────────────── + // BUILD THE DEVICE STRUCT + // ───────────────────────────────────────────────────────────── + // PATTERN: Builder pattern - set required fields, then optional + let mut device = StorageDevice { + name: device_name.clone(), + device_path: format!("/dev/{}", device_name), + device_type: device_type.clone(), + type_: device_type.display_name().to_string(), + size_bytes, + model, + serial_number, + firmware_version, + interface, + is_rotational, + detection_method: "sysfs".to_string(), + ..Default::default() + }; + + // Calculate derived fields + device.calculate_size_fields(); + + devices.push(device); + } + + Ok(devices) + } + + /// Read NVMe-specific sysfs attributes. + /// + /// NVMe devices have attributes in a different location: + /// `/sys/class/nvme/nvme0/serial` instead of `/sys/block/nvme0n1/device/serial` + /// + /// # Arguments + /// + /// * `device_name` - Block device name (e.g., "nvme0n1") + /// * `existing_serial` - Serial from block device path (may be None) + /// * `existing_firmware` - Firmware from block device path (may be None) + fn read_nvme_sysfs_attrs( + &self, + device_name: &str, + existing_serial: Option, + existing_firmware: Option, + ) -> (Option, Option) { + // Extract controller name: "nvme0n1" -> "nvme0" + // PATTERN: String manipulation - find pattern and extract + let controller = device_name + .chars() + .take_while(|c| !c.is_ascii_digit() || device_name.starts_with("nvme")) + .take_while(|&c| c != 'n' || device_name.find("nvme").is_some()) + .collect::(); + + // Try to extract just "nvme0" from "nvme0n1" + let controller = if device_name.starts_with("nvme") { + // Find position of 'n' that's followed by a digit (the namespace) + if let Some(pos) = device_name[4..].find('n') { + &device_name[..4 + pos] + } else { + &device_name + } + } else { + &device_name + }; + + let nvme_path = PathBuf::from("/sys/class/nvme").join(controller); + + // Try to get serial from NVMe class path + let serial = existing_serial.or_else(|| { + self.read_sysfs_file(&nvme_path.join("serial")) + .map(|s| s.trim().to_string()) + .ok() + .filter(|s| !s.is_empty()) + }); + + // Try to get firmware from NVMe class path + let firmware = existing_firmware.or_else(|| { + self.read_sysfs_file(&nvme_path.join("firmware_rev")) + .map(|s| s.trim().to_string()) + .ok() + .filter(|s| !s.is_empty()) + }); + + (serial, firmware) + } + + /// Detect storage via lsblk command (JSON output). + /// + /// # Command + /// + /// ```bash + /// lsblk -J -b -o NAME,SIZE,TYPE,MODEL,SERIAL,ROTA,TRAN,WWN + /// ``` + /// + /// # Flags + /// + /// - `-J` = JSON output (easier to parse than text) + /// - `-b` = Size in bytes (not human-readable) + /// - `-o` = Specify columns + /// + /// # When to Use + /// + /// - Enrichment after sysfs (WWN, transport) + /// - Fallback if sysfs fails + async fn detect_storage_lsblk(&self) -> Result, SystemError> { + let cmd = SystemCommand::new("lsblk") + .args(&[ + "-J", // JSON output + "-b", // Bytes (not human readable) + "-d", // No partitions + "-o", "NAME,SIZE,TYPE,MODEL,SERIAL,ROTA,TRAN,WWN", + ]) + .timeout(Duration::from_secs(10)); + + // PATTERN: Combined error mapping with and_then + // Execute command and check success in one chain + let output = self.command_executor.execute(&cmd).await.map_err(|e| { + SystemError::CommandFailed { + command: "lsblk".to_string(), + exit_code: None, + stderr: e.to_string(), + } + })?; + + // PATTERN: Guard clause with let-else for cleaner flow + let output = if output.success { output } else { + return Err(SystemError::CommandFailed { + command: "lsblk".to_string(), + exit_code: output.exit_code, + stderr: output.stderr, + }); + }; + + parse_lsblk_json(&output.stdout).map_err(SystemError::ParseError) + } + + /// Detect storage via sysinfo crate (cross-platform fallback). + /// + /// # Limitations + /// + /// sysinfo provides: + /// - Mounted filesystems (not raw block devices) + /// - Limited metadata (no serial, model, etc.) + /// + /// Use only as last resort. + fn detect_storage_sysinfo(&self) -> Result, SystemError> { + use sysinfo::Disks; + + let disks = Disks::new_with_refreshed_list(); + let mut devices = Vec::new(); + + for disk in disks.iter() { + let size_bytes = disk.total_space(); + + // Skip small devices + if size_bytes < 1_000_000_000 { + continue; + } + + let name = disk.name().to_string_lossy().to_string(); + let name = if name.is_empty() { + disk.mount_point().to_string_lossy().to_string() + } else { + name + }; + + let mut device = StorageDevice { + name, + size_bytes, + detection_method: "sysinfo".to_string(), + ..Default::default() + }; + + device.calculate_size_fields(); + devices.push(device); + } + + Ok(devices) + } + + /// Merge storage info from secondary source into primary. + /// + /// # Strategy + /// + /// 1. Match devices by name + /// 2. Fill in missing fields from secondary + /// 3. Don't overwrite existing data (primary is authoritative) + /// + /// # LeetCode Connection + /// + /// This is the **merge** pattern: + /// - LC #88 Merge Sorted Array + /// - LC #21 Merge Two Sorted Lists + /// - LC #56 Merge Intervals + /// + /// Key insight: we're merging by KEY (device name), not by position. + /// + /// # Complexity + /// + /// Current: O(n × m) where n = primary.len(), m = secondary.len() + /// + /// Could optimize with HashMap for O(n + m), but device lists are small + /// (typically < 20), so linear search is fine and simpler. + fn merge_storage_info( + &self, + primary: &mut Vec, + secondary: Vec, + ) { + for sec_device in secondary { + // PATTERN: if-let-else for merge-or-insert + if let Some(pri_device) = primary.iter_mut().find(|d| d.name == sec_device.name) { + // PATTERN: Option::or() for null coalescing - much cleaner! + // Before: if pri.field.is_none() { pri.field = sec.field; } + // After: pri.field = pri.field.take().or(sec.field); + pri_device.serial_number = pri_device.serial_number.take().or(sec_device.serial_number); + pri_device.firmware_version = pri_device.firmware_version.take().or(sec_device.firmware_version); + pri_device.wwn = pri_device.wwn.take().or(sec_device.wwn); + + // PATTERN: Conditional assignment with && guard + if pri_device.model.is_empty() && !sec_device.model.is_empty() { + pri_device.model = sec_device.model; + } + } else { + // Device not in primary - add it + primary.push(sec_device); + } + } + } +} +``` + +--- + +### Step 3: CPU Detection + +**Location:** Update `get_cpu_info` method (around line 67-102) + +```rust +// ============================================================================= +// CPU DETECTION +// ============================================================================= +// +// ENHANCEMENTS: +// - Add frequency_mhz (numeric, not string) +// - Add cache sizes (L1d, L1i, L2, L3) +// - Add CPU flags/features +// - Better ARM support via /proc/cpuinfo +// +// DETECTION CHAIN: +// 1. sysfs for frequency and cache +// 2. /proc/cpuinfo for model, vendor, flags +// 3. lscpu for topology +// 4. dmidecode for additional data (with privileges) +// ============================================================================= + +async fn get_cpu_info(&self) -> Result { + // Start with basic info from lscpu (existing code) + let lscpu_cmd = SystemCommand::new("lscpu").timeout(Duration::from_secs(10)); + let lscpu_output = self + .command_executor + .execute(&lscpu_cmd) + .await + .map_err(|e| SystemError::CommandFailed { + command: "lscpu".to_string(), + exit_code: None, + stderr: e.to_string(), + })?; + + let mut cpu_info = parse_lscpu_output(&lscpu_output.stdout) + .map_err(SystemError::ParseError)?; + + // ========================================================================= + // ENHANCEMENT 1: Frequency from sysfs + // ========================================================================= + // + // sysfs provides exact frequency in kHz + // More reliable than parsing lscpu string output + // + // Paths: + // - /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq (max frequency) + // - /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq (current) + // ========================================================================= + + // PATTERN: if-let with destructuring for tuple results + // Clean way to handle optional enhancement without nested blocks + if let Ok((freq_mhz, freq_min, freq_max)) = self.detect_cpu_sysfs_frequency().await { + cpu_info.frequency_mhz = freq_mhz; + cpu_info.frequency_min_mhz = freq_min; + cpu_info.frequency_max_mhz = freq_max; + cpu_info.set_speed_string(); + cpu_info.detection_methods.push("sysfs_freq".to_string()); + } + + // ========================================================================= + // ENHANCEMENT 2: Cache from sysfs + // ========================================================================= + // + // sysfs provides detailed cache hierarchy: + // /sys/devices/system/cpu/cpu0/cache/index0/ (L1d typically) + // /sys/devices/system/cpu/cpu0/cache/index1/ (L1i typically) + // /sys/devices/system/cpu/cpu0/cache/index2/ (L2) + // /sys/devices/system/cpu/cpu0/cache/index3/ (L3) + // + // Each has: level, type, size, ways_of_associativity, etc. + // ========================================================================= + + // PATTERN: if-let + for loop with tuple matching + // Avoids deep nesting by using tuple pattern matching + if let Ok(caches) = self.detect_cpu_sysfs_cache().await { + for cache in &caches { + // Tuple matching is cleaner than nested if-else + match (cache.level, cache.cache_type.as_str()) { + (1, "Data") => cpu_info.cache_l1d_kb = Some(cache.size_kb), + (1, "Instruction") => cpu_info.cache_l1i_kb = Some(cache.size_kb), + (2, _) => cpu_info.cache_l2_kb = Some(cache.size_kb), + (3, _) => cpu_info.cache_l3_kb = Some(cache.size_kb), + _ => {} // L4 or unified caches - ignored for now + } + } + cpu_info.caches = caches; + cpu_info.detection_methods.push("sysfs_cache".to_string()); + } + + // ========================================================================= + // ENHANCEMENT 3: Flags and vendor from /proc/cpuinfo + // ========================================================================= + // + // /proc/cpuinfo format differs by architecture: + // + // x86_64: + // vendor_id : GenuineIntel + // flags : fpu vme de pse avx avx2 avx512f ... + // + // aarch64: + // CPU implementer : 0x41 + // CPU part : 0xd0c + // Features : fp asimd evtstrm aes ... + // ========================================================================= + + // PATTERN: if-let with && chaining for conditional field updates + // Each field update only happens if condition is met + if let Ok(proc_info) = self.read_proc_cpuinfo().await { + // PATTERN: Short-circuit with && for conditional assignment + if !proc_info.flags.is_empty() { cpu_info.flags = proc_info.flags; } + + // PATTERN: && chaining avoids nested if blocks + if cpu_info.vendor.is_empty() && !proc_info.vendor.is_empty() { + cpu_info.vendor = proc_info.vendor; + } + + // PATTERN: Option::is_some() then take - or use or_else + cpu_info.microarchitecture = cpu_info.microarchitecture.or(proc_info.microarchitecture); + + cpu_info.detection_methods.push("proc_cpuinfo".to_string()); + } + + // ========================================================================= + // EXISTING: dmidecode enrichment (with privileges) + // ========================================================================= + + let dmidecode_cmd = SystemCommand::new("dmidecode") + .args(&["-t", "processor"]) + .timeout(Duration::from_secs(10)); + + // PATTERN: Nested if-let flattened with && conditions + // Original: if let Ok { if success { if let Ok { ... } } } + // Refactored: Single if-let chain with && guard + if let Ok(output) = self.command_executor.execute_with_privileges(&dmidecode_cmd).await { + if output.success && let Ok(dmidecode_info) = parse_dmidecode_cpu(&output.stdout) { + cpu_info = combine_cpu_info(cpu_info, dmidecode_info); + cpu_info.detection_methods.push("dmidecode".to_string()); + } + } + + // Calculate totals + cpu_info.calculate_totals(); + + // Set architecture + cpu_info.architecture = std::env::consts::ARCH.to_string(); + + Ok(cpu_info) +} +``` + +**Add CPU helper methods:** + +```rust +// ============================================================================= +// CPU HELPER METHODS +// ============================================================================= + +impl LinuxSystemInfoProvider { + /// Detect CPU frequency from sysfs. + /// + /// # sysfs Paths + /// + /// - `/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq` - Max frequency (kHz) + /// - `/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq` - Min frequency (kHz) + /// - `/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq` - Current (kHz) + /// + /// # Returns + /// + /// Tuple of (primary_mhz, min_mhz, max_mhz) + /// + /// # LeetCode Connection + /// + /// File I/O with error handling is like parsing problems: + /// - Handle missing files gracefully + /// - Convert units (kHz → MHz) + async fn detect_cpu_sysfs_frequency(&self) + -> Result<(u32, Option, Option), SystemError> + { + let cpu_path = Path::new("/sys/devices/system/cpu/cpu0/cpufreq"); + + if !cpu_path.exists() { + return Err(SystemError::NotAvailable { + resource: "/sys/devices/system/cpu/cpu0/cpufreq".to_string(), + }); + } + + // Read max frequency (primary) + let max_freq = self.read_sysfs_file(&cpu_path.join("cpuinfo_max_freq")) + .ok() + .and_then(|s| parse_sysfs_freq_khz(&s).ok()); + + // Read min frequency + let min_freq = self.read_sysfs_file(&cpu_path.join("cpuinfo_min_freq")) + .ok() + .and_then(|s| parse_sysfs_freq_khz(&s).ok()); + + // Read current frequency (fallback for primary) + let cur_freq = self.read_sysfs_file(&cpu_path.join("scaling_cur_freq")) + .ok() + .and_then(|s| parse_sysfs_freq_khz(&s).ok()); + + // Use max as primary, fall back to current + let primary = max_freq.or(cur_freq).unwrap_or(0); + + Ok((primary, min_freq, max_freq)) + } + + /// Detect CPU cache hierarchy from sysfs. + /// + /// # sysfs Structure + /// + /// ```text + /// /sys/devices/system/cpu/cpu0/cache/ + /// ├── index0/ # Usually L1 Data + /// │ ├── level # "1" + /// │ ├── type # "Data" + /// │ └── size # "32K" + /// ├── index1/ # Usually L1 Instruction + /// ├── index2/ # Usually L2 + /// └── index3/ # Usually L3 (shared) + /// ``` + /// + /// # LeetCode Connection + /// + /// Directory traversal with structured data extraction: + /// - Similar to LC #102 Level Order Traversal (visiting nodes at each level) + /// - Cache hierarchy IS a tree structure! + async fn detect_cpu_sysfs_cache(&self) -> Result, SystemError> { + let cache_path = Path::new("/sys/devices/system/cpu/cpu0/cache"); + + if !cache_path.exists() { + return Err(SystemError::NotAvailable { + resource: cache_path.to_string_lossy().to_string(), + }); + } + + let mut caches = Vec::new(); + + // Iterate through index0, index1, index2, index3 + for i in 0..10 { + let index_path = cache_path.join(format!("index{}", i)); + + if !index_path.exists() { + break; + } + + // Read cache attributes + let level: u8 = self.read_sysfs_file(&index_path.join("level")) + .ok() + .and_then(|s| s.trim().parse().ok()) + .unwrap_or(0); + + let cache_type = self.read_sysfs_file(&index_path.join("type")) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|_| "Unknown".to_string()); + + let size_kb = self.read_sysfs_file(&index_path.join("size")) + .ok() + .and_then(|s| parse_sysfs_cache_size(&s).ok()) + .unwrap_or(0); + + let ways = self.read_sysfs_file(&index_path.join("ways_of_associativity")) + .ok() + .and_then(|s| s.trim().parse().ok()); + + let line_size = self.read_sysfs_file(&index_path.join("coherency_line_size")) + .ok() + .and_then(|s| s.trim().parse().ok()); + + caches.push(CpuCacheInfo { + level, + cache_type, + size_kb, + ways_of_associativity: ways, + line_size_bytes: line_size, + sets: None, + shared: Some(level >= 3), // L3 is typically shared + }); + } + + Ok(caches) + } + + /// Read and parse /proc/cpuinfo. + /// + /// # Why? + /// + /// /proc/cpuinfo contains: + /// - CPU flags/features (important for workload compatibility) + /// - Vendor identification + /// - ARM CPU part numbers (for microarchitecture detection) + async fn read_proc_cpuinfo(&self) -> Result { + let content = fs::read_to_string("/proc/cpuinfo").map_err(|e| { + SystemError::IoError { + path: "/proc/cpuinfo".to_string(), + message: e.to_string(), + } + })?; + + parse_proc_cpuinfo(&content).map_err(SystemError::ParseError) + } +} +``` + +--- + +### Step 4: GPU Detection + +**Location:** Replace `get_gpu_info` method (around line 172-232) + +```rust +// ============================================================================= +// GPU DETECTION +// ============================================================================= +// +// MULTI-METHOD CHAIN (Chain of Responsibility pattern): +// +// Priority 1: NVML (if feature enabled) +// - Most accurate for NVIDIA +// - Direct library, no parsing +// +// Priority 2: nvidia-smi +// - Fallback for NVIDIA +// - Parse CSV output +// +// Priority 3: rocm-smi +// - AMD GPU detection +// - Parse JSON output +// +// Priority 4: sysfs /sys/class/drm +// - Universal Linux +// - Works for all vendors +// - Limited memory info +// +// Priority 5: lspci +// - Basic enumeration +// - No memory/driver info +// - Last resort +// +// LEETCODE CONNECTION: +// - Chain of Responsibility is like trying multiple approaches +// - LC #322 Coin Change: try different options +// - LC #70 Climbing Stairs: multiple ways to reach goal +// ============================================================================= + +async fn get_gpu_info(&self) -> Result { + let mut devices = Vec::new(); + + // ========================================================================= + // METHOD 1: nvidia-smi (NVIDIA GPUs) + // ========================================================================= + // + // Command: nvidia-smi --query-gpu=... --format=csv,noheader,nounits + // + // Key flags: + // - nounits: Returns "81920" instead of "81920 MiB" + // - noheader: Skip column headers + // - csv: Comma-separated for easy parsing + // + // Fields we query: + // - index, name, uuid, memory.total, memory.free + // - pci.bus_id, driver_version, compute_cap + // ========================================================================= + + if let Ok(nvidia_devices) = self.detect_gpus_nvidia_smi().await { + log::debug!("nvidia-smi detected {} GPUs", nvidia_devices.len()); + devices.extend(nvidia_devices); + } + + // ========================================================================= + // METHOD 2: rocm-smi (AMD GPUs) + // ========================================================================= + // + // Only try if we don't have NVIDIA GPUs (or want both) + // AMD GPUs won't show up via nvidia-smi + // ========================================================================= + + if let Ok(amd_devices) = self.detect_gpus_rocm_smi().await { + log::debug!("rocm-smi detected {} GPUs", amd_devices.len()); + // Merge AMD GPUs (they won't conflict with NVIDIA by name) + devices.extend(amd_devices); + } + + // ========================================================================= + // METHOD 3: sysfs /sys/class/drm (enrichment or fallback) + // ========================================================================= + // + // sysfs provides: + // - PCI vendor/device IDs + // - NUMA node + // - AMD: Memory info via mem_info_vram_total + // + // Use to: + // - Enrich existing devices with NUMA info + // - Fallback detection if commands failed + // ========================================================================= + + if let Ok(drm_devices) = self.detect_gpus_sysfs_drm().await { + log::debug!("sysfs DRM detected {} GPUs", drm_devices.len()); + self.merge_gpu_info(&mut devices, drm_devices); + } + + // ========================================================================= + // METHOD 4: lspci (last resort) + // ========================================================================= + // + // If we still have no GPUs, try lspci + // This only gives us basic enumeration (no memory, driver) + // ========================================================================= + + if devices.is_empty() { + if let Ok(lspci_devices) = self.detect_gpus_lspci().await { + log::debug!("lspci detected {} GPUs", lspci_devices.len()); + devices = lspci_devices; + } + } + + // ========================================================================= + // POST-PROCESSING + // ========================================================================= + + // Re-index devices + for (i, device) in devices.iter_mut().enumerate() { + device.index = i as u32; + + // Ensure legacy memory field is set + #[allow(deprecated)] + if device.memory.is_empty() { + device.set_memory_string(); + } + } + + // Sort by index for consistent output + devices.sort_by_key(|d| d.index); + + log::info!("Detected {} GPUs", devices.len()); + Ok(GpuInfo { devices }) +} +``` + +**Add GPU helper methods:** + +```rust +// ============================================================================= +// GPU HELPER METHODS +// ============================================================================= + +impl LinuxSystemInfoProvider { + /// Detect NVIDIA GPUs via nvidia-smi command. + /// + /// # Command + /// + /// ```bash + /// nvidia-smi --query-gpu=index,name,uuid,memory.total,memory.free,pci.bus_id,driver_version,compute_cap \ + /// --format=csv,noheader,nounits + /// ``` + /// + /// # Output Format + /// + /// ```text + /// 0, NVIDIA H100 80GB HBM3, GPU-xxxx, 81920, 81000, 00000000:01:00.0, 535.129.03, 9.0 + /// ``` + /// + /// # LeetCode Connection + /// + /// CSV parsing is like string manipulation problems: + /// - LC #68 Text Justification + /// - LC #722 Remove Comments + async fn detect_gpus_nvidia_smi(&self) -> Result, SystemError> { + let cmd = SystemCommand::new("nvidia-smi") + .args(&[ + "--query-gpu=index,name,uuid,memory.total,memory.free,pci.bus_id,driver_version,compute_cap", + "--format=csv,noheader,nounits", + ]) + .timeout(Duration::from_secs(10)); + + let output = self.command_executor.execute(&cmd).await.map_err(|e| { + SystemError::CommandFailed { + command: "nvidia-smi".to_string(), + exit_code: None, + stderr: e.to_string(), + } + })?; + + // PATTERN: let-else for guard clause + // Cleaner than if !success { return Err } + let output = if output.success { output } else { + return Err(SystemError::CommandFailed { + command: "nvidia-smi".to_string(), + exit_code: output.exit_code, + stderr: output.stderr, + }); + }; + + parse_nvidia_smi_output(&output.stdout).map_err(SystemError::ParseError) + } + + /// Detect AMD GPUs via rocm-smi command. + /// + /// # Command + /// + /// ```bash + /// rocm-smi --showproductname --showmeminfo vram --showdriver --json + /// ``` + async fn detect_gpus_rocm_smi(&self) -> Result, SystemError> { + let cmd = SystemCommand::new("rocm-smi") + .args(&["--showproductname", "--showmeminfo", "vram", "--showdriver", "--json"]) + .timeout(Duration::from_secs(10)); + + // PATTERN: Match with guard clause for conditional success + // This is idiomatic when you need both Ok AND a condition + let Ok(output) = self.command_executor.execute(&cmd).await else { + return Ok(Vec::new()); // rocm-smi not available - not an AMD system + }; + if !output.success { + return Ok(Vec::new()); // Command failed - not an AMD system + } + + // Parse rocm-smi JSON output + // TODO: Implement parse_rocm_smi_output in parsers/gpu.rs + self.parse_rocm_smi_json(&output.stdout) + } + + /// Parse rocm-smi JSON output (inline for now). + fn parse_rocm_smi_json(&self, output: &str) -> Result, SystemError> { + let json: serde_json::Value = serde_json::from_str(output) + .map_err(|e| SystemError::ParseError(format!("rocm-smi JSON parse error: {}", e)))?; + + // PATTERN: let-else for early return when required data missing + let Some(obj) = json.as_object() else { + return Ok(Vec::new()); // Not a JSON object - no GPUs + }; + + // PATTERN: filter_map + enumerate for index tracking + // Cleaner than manual index increment + let devices: Vec = obj.iter() + .filter(|(key, _)| key.starts_with("card")) + .enumerate() + .map(|(index, (_, value))| { + // PATTERN: and_then chains for nested Option extraction + let name = value.get("Card series") + .and_then(|v| v.as_str()) + .unwrap_or("AMD GPU") + .to_string(); + + let memory_bytes = value.get("VRAM Total Memory (B)") + .and_then(|v| v.as_str()) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + + let driver_version = value.get("Driver version") + .and_then(|v| v.as_str()) + .map(String::from); + + GpuDevice { + index: index as u32, + name, + uuid: format!("amd-gpu-{}", index), + memory_total_mb: memory_bytes / (1024 * 1024), + driver_version, + vendor: GpuVendor::Amd, + vendor_name: "AMD".to_string(), + detection_method: "rocm-smi".to_string(), + ..Default::default() + } + }) + .collect(); + + Ok(devices) + } + + /// Detect GPUs via sysfs DRM interface. + /// + /// # sysfs Paths + /// + /// ```text + /// /sys/class/drm/card0/device/ + /// ├── vendor # PCI vendor ID ("0x10de" = NVIDIA) + /// ├── device # PCI device ID + /// ├── numa_node # NUMA affinity + /// └── mem_info_vram_total # AMD: VRAM size in bytes + /// ``` + /// + /// # Use Cases + /// + /// - Get NUMA node info for all GPUs + /// - Get memory info for AMD GPUs + /// - Fallback enumeration + async fn detect_gpus_sysfs_drm(&self) -> Result, SystemError> { + let drm_path = Path::new("/sys/class/drm"); + + if !drm_path.exists() { + return Err(SystemError::NotAvailable { + resource: "/sys/class/drm".to_string(), + }); + } + + let mut devices = Vec::new(); + let mut index = 0; + + let entries = fs::read_dir(drm_path).map_err(|e| SystemError::IoError { + path: "/sys/class/drm".to_string(), + message: e.to_string(), + })?; + + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + + // Only process card* entries (not renderD*) + if !name.starts_with("card") || name.contains("-") { + continue; + } + + let card_path = entry.path(); + let device_path = card_path.join("device"); + + if !device_path.exists() { + continue; + } + + // Read PCI vendor ID + let vendor_id = self.read_sysfs_file(&device_path.join("vendor")) + .map(|s| s.trim().trim_start_matches("0x").to_string()) + .unwrap_or_default(); + + let vendor = GpuVendor::from_pci_vendor(&vendor_id); + + // Skip if not a GPU vendor we recognize + if vendor == GpuVendor::Unknown { + continue; + } + + // Read PCI device ID + let device_id = self.read_sysfs_file(&device_path.join("device")) + .map(|s| s.trim().trim_start_matches("0x").to_string()) + .unwrap_or_default(); + + // Read NUMA node + let numa_node = self.read_sysfs_file(&device_path.join("numa_node")) + .ok() + .and_then(|s| s.trim().parse::().ok()); + + // AMD-specific: Read VRAM size + let memory_total_mb = if vendor == GpuVendor::Amd { + self.read_sysfs_file(&device_path.join("mem_info_vram_total")) + .ok() + .and_then(|s| s.trim().parse::().ok()) + .map(|bytes| bytes / (1024 * 1024)) + .unwrap_or(0) + } else { + 0 + }; + + let device = GpuDevice { + index, + name: format!("{} GPU ({})", vendor.name(), name), + uuid: format!("drm-{}", name), + memory_total_mb, + pci_id: format!("{}:{}", vendor_id, device_id), + vendor: vendor.clone(), + vendor_name: vendor.name().to_string(), + numa_node, + detection_method: "sysfs".to_string(), + ..Default::default() + }; + + devices.push(device); + index += 1; + } + + Ok(devices) + } + + /// Detect GPUs via lspci command (last resort). + async fn detect_gpus_lspci(&self) -> Result, SystemError> { + let cmd = SystemCommand::new("lspci") + .args(&["-nn"]) + .timeout(Duration::from_secs(5)); + + let output = self.command_executor.execute(&cmd).await.map_err(|e| { + SystemError::CommandFailed { + command: "lspci".to_string(), + exit_code: None, + stderr: e.to_string(), + } + })?; + + if !output.success { + return Err(SystemError::CommandFailed { + command: "lspci".to_string(), + exit_code: output.exit_code, + stderr: output.stderr.clone(), + }); + } + + parse_lspci_gpu_output(&output.stdout).map_err(SystemError::ParseError) + } + + /// Merge GPU info from secondary source into primary. + /// + /// Match by PCI bus ID or name, fill in missing fields. + fn merge_gpu_info(&self, primary: &mut Vec, secondary: Vec) { + for sec_gpu in secondary { + // PATTERN: and_then + find with predicate chaining + // Cleaner than nested if-let + let matched = sec_gpu.pci_bus_id.as_ref().and_then(|sec_bus_id| { + primary.iter_mut().find(|g| { + g.pci_bus_id.as_ref().is_some_and(|id| id == sec_bus_id) + }) + }); + + // PATTERN: if-let-else for merge-or-insert logic + if let Some(pri_gpu) = matched { + // PATTERN: or/or_else for null coalescing + pri_gpu.numa_node = pri_gpu.numa_node.or(sec_gpu.numa_node); + if pri_gpu.pci_id.is_empty() { pri_gpu.pci_id = sec_gpu.pci_id; } + if pri_gpu.memory_total_mb == 0 { pri_gpu.memory_total_mb = sec_gpu.memory_total_mb; } + } else if !primary.iter().any(|g| g.name == sec_gpu.name) { + primary.push(sec_gpu); + } + } + } +} +``` + +--- + +### Step 5: Network Detection + +**Location:** Update `get_network_info` method (around line 234-252) + +```rust +// ============================================================================= +// NETWORK DETECTION +// ============================================================================= +// +// ENHANCEMENTS: +// - Add driver and driver_version +// - Add MTU +// - Add is_up, is_virtual +// - Add speed_mbps (numeric) +// ============================================================================= + +async fn get_network_info(&self) -> Result { + // Get basic interface info from ip command (existing code) + let ip_cmd = SystemCommand::new("ip") + .args(&["addr", "show"]) + .timeout(Duration::from_secs(5)); + + let ip_output = self.command_executor.execute(&ip_cmd).await.map_err(|e| { + SystemError::CommandFailed { + command: "ip".to_string(), + exit_code: None, + stderr: e.to_string(), + } + })?; + + let mut interfaces = parse_ip_output(&ip_output.stdout) + .map_err(SystemError::ParseError)?; + + // ========================================================================= + // ENHANCEMENT: Enrich with sysfs data + // ========================================================================= + // + // sysfs provides: + // - /sys/class/net/{iface}/operstate (up/down) + // - /sys/class/net/{iface}/speed (Mbps, may be -1) + // - /sys/class/net/{iface}/mtu + // - /sys/class/net/{iface}/device/driver -> symlink to driver + // ========================================================================= + + for iface in &mut interfaces { + self.enrich_network_interface_sysfs(iface).await; + } + + Ok(NetworkInfo { + interfaces, + infiniband: None, // TODO: Add infiniband detection + }) +} +``` + +**Add network helper methods:** + +```rust +// ============================================================================= +// NETWORK HELPER METHODS +// ============================================================================= + +impl LinuxSystemInfoProvider { + /// Enrich network interface with sysfs data. + /// + /// # sysfs Paths + /// + /// ```text + /// /sys/class/net/{iface}/ + /// ├── operstate # "up", "down", "unknown" + /// ├── speed # Mbps (may be -1 if unknown) + /// ├── mtu # MTU in bytes + /// ├── carrier # 1 = link detected + /// └── device/ + /// └── driver/ # Symlink to driver module + /// └── module/ + /// └── version # Driver version + /// ``` + async fn enrich_network_interface_sysfs(&self, iface: &mut NetworkInterface) { + let iface_path = PathBuf::from("/sys/class/net").join(&iface.name); + + if !iface_path.exists() { + return; + } + + // ───────────────────────────────────────────────────────────── + // OPERATIONAL STATE + // ───────────────────────────────────────────────────────────── + iface.is_up = self.read_sysfs_file(&iface_path.join("operstate")) + .map(|s| s.trim().to_lowercase() == "up") + .unwrap_or(false); + + // ───────────────────────────────────────────────────────────── + // SPEED + // ───────────────────────────────────────────────────────────── + // May be -1 if link is down or speed is unknown + // PATTERN: if-let chain with && for multiple conditions + if let Ok(speed_str) = self.read_sysfs_file(&iface_path.join("speed")) + && let Ok(speed) = speed_str.trim().parse::() + && speed > 0 + { + iface.speed_mbps = Some(speed as u32); + iface.speed = Some(format!("{} Mbps", speed)); + } + + // ───────────────────────────────────────────────────────────── + // MTU + // ───────────────────────────────────────────────────────────── + // PATTERN: if-let chain - parse only if read succeeds + if let Ok(mtu_str) = self.read_sysfs_file(&iface_path.join("mtu")) + && let Ok(mtu) = mtu_str.trim().parse::() + { + iface.mtu = mtu; + } + + // ───────────────────────────────────────────────────────────── + // CARRIER (link detected) + // ───────────────────────────────────────────────────────────── + iface.carrier = self.read_sysfs_file(&iface_path.join("carrier")) + .map(|s| s.trim() == "1") + .ok(); + + // ───────────────────────────────────────────────────────────── + // VIRTUAL INTERFACE DETECTION + // ───────────────────────────────────────────────────────────── + // Virtual interfaces don't have a physical device + let device_path = iface_path.join("device"); + iface.is_virtual = !device_path.exists() + || iface.name.starts_with("lo") + || iface.name.starts_with("veth") + || iface.name.starts_with("br") + || iface.name.starts_with("docker") + || iface.name.starts_with("virbr"); + + // ───────────────────────────────────────────────────────────── + // DRIVER INFORMATION (only for physical interfaces) + // ───────────────────────────────────────────────────────────── + // PATTERN: Negated guard with if-let chain + // Only process driver info for non-virtual interfaces + if !iface.is_virtual { + let driver_link = device_path.join("driver"); + // PATTERN: if-let chain with && for nested conditionals + if let Ok(driver_path) = fs::read_link(&driver_link) + && let Some(driver_name) = driver_path.file_name() + { + let driver_str = driver_name.to_string_lossy().to_string(); + iface.driver = Some(driver_str.clone()); + + // Chain: driver version lookup + let version_path = PathBuf::from("/sys/module").join(&driver_str).join("version"); + if let Ok(version) = self.read_sysfs_file(&version_path) { + iface.driver_version = Some(version.trim().to_string()); + } + } + } + + // ───────────────────────────────────────────────────────────── + // INTERFACE TYPE + // ───────────────────────────────────────────────────────────── + // PATTERN: Match-like if-else chain for classification + iface.interface_type = if iface.name == "lo" { + NetworkInterfaceType::Loopback + } else if iface.name.starts_with("br") || iface.name.starts_with("virbr") { + NetworkInterfaceType::Bridge + } else if iface.name.starts_with("veth") { + NetworkInterfaceType::Veth + } else if iface.name.starts_with("bond") { + NetworkInterfaceType::Bond + } else if iface.name.contains(".") { + NetworkInterfaceType::Vlan + } else if iface.name.starts_with("wl") { + NetworkInterfaceType::Wireless + } else if iface.name.starts_with("ib") { + NetworkInterfaceType::Infiniband + } else { + NetworkInterfaceType::Ethernet + }; + } +} +``` + +--- + +## Helper Functions + +**Add this general-purpose helper at the end of the impl block:** + +```rust +// ============================================================================= +// GENERAL HELPER METHODS +// ============================================================================= + +impl LinuxSystemInfoProvider { + /// Read a sysfs file and return contents as String. + /// + /// # Error Handling + /// + /// Returns Err if: + /// - File doesn't exist + /// - Permission denied + /// - Any I/O error + /// + /// # Why This Helper? + /// + /// - Centralizes error handling + /// - Consistent logging + /// - Can add caching later if needed + fn read_sysfs_file(&self, path: &Path) -> Result { + fs::read_to_string(path) + } +} +``` + +--- + +## Testing + +After implementing, verify with: + +```bash +# Check compilation +cargo check + +# Run tests +cargo test + +# Test on real hardware (run binary) +cargo run --bin hardware_report + +# Check specific detection +cargo run --bin hardware_report 2>&1 | grep -A 5 "storage" +cargo run --bin hardware_report 2>&1 | grep -A 5 "gpus" +``` + +--- + +## Rust Idioms: if-let Chaining & let-else + +This implementation uses modern Rust patterns to reduce nesting: + +### let-else (Early Exit Pattern) + +```rust +// BEFORE: Nested match/if for required values +let size_bytes = match self.read_sysfs_file(&path) { + Ok(content) => match parse_sysfs_size(&content) { + Ok(size) => size, + Err(_) => continue, + }, + Err(_) => continue, +}; + +// AFTER: let-else for early exit +let Ok(content) = self.read_sysfs_file(&path) else { continue; }; +let Ok(size_bytes) = parse_sysfs_size(&content) else { continue; }; +``` + +### if-let Chaining with && + +```rust +// BEFORE: Nested if-let +if let Ok(speed_str) = read_file(&path) { + if let Ok(speed) = speed_str.parse::() { + if speed > 0 { + iface.speed = Some(speed); + } + } +} + +// AFTER: Chained if-let with && +if let Ok(speed_str) = read_file(&path) + && let Ok(speed) = speed_str.parse::() + && speed > 0 +{ + iface.speed = Some(speed); +} +``` + +### Option::or() for Null Coalescing + +```rust +// BEFORE: Verbose conditional assignment +if pri.serial.is_none() { + pri.serial = sec.serial; +} + +// AFTER: Functional style +pri.serial = pri.serial.take().or(sec.serial); +``` + +### and_then Chains + +```rust +// BEFORE: Nested if-let for Option extraction +if let Some(bus_id) = &sec_gpu.pci_bus_id { + if let Some(pri) = primary.iter_mut().find(|g| ...) { + // merge + } +} + +// AFTER: and_then chain +let matched = sec_gpu.pci_bus_id.as_ref().and_then(|bus_id| { + primary.iter_mut().find(|g| g.pci_bus_id.as_ref().is_some_and(|id| id == bus_id)) +}); +``` + +--- + +## LeetCode Pattern Summary + +| Pattern | Problems | Where Used | +|---------|----------|------------| +| **Chain of Responsibility** | - | All detection methods (sysfs → command → fallback) | +| **Merge/Combine** | LC #88, #21, #56 | `merge_storage_info`, `merge_gpu_info` | +| **Tree Traversal** | LC #102, #200 | sysfs directory walking, cache hierarchy | +| **Filtering** | LC #283, #27 | `devices.retain()` for virtual devices | +| **Hash Map Lookup** | LC #1, #49 | Vendor ID → vendor name | +| **String Parsing** | LC #8, #65 | sysfs file parsing | +| **Pattern Matching** | LC #28, #10 | lspci PCI ID extraction | +| **Two Pointers** | LC #88 | Merge operations | + +--- + +## Implementation Checklist + +Use this to track your progress: + +```markdown +## Storage Detection +- [ ] Update imports +- [ ] Replace get_storage_info method +- [ ] Add detect_storage_sysfs helper +- [ ] Add detect_storage_lsblk helper +- [ ] Add detect_storage_sysinfo helper +- [ ] Add read_nvme_sysfs_attrs helper +- [ ] Add merge_storage_info helper +- [ ] Test: cargo check + +## CPU Detection +- [ ] Update get_cpu_info method +- [ ] Add detect_cpu_sysfs_frequency helper +- [ ] Add detect_cpu_sysfs_cache helper +- [ ] Add read_proc_cpuinfo helper +- [ ] Test: cargo check + +## GPU Detection +- [ ] Replace get_gpu_info method +- [ ] Add detect_gpus_nvidia_smi helper +- [ ] Add detect_gpus_rocm_smi helper +- [ ] Add parse_rocm_smi_json helper +- [ ] Add detect_gpus_sysfs_drm helper +- [ ] Add detect_gpus_lspci helper +- [ ] Add merge_gpu_info helper +- [ ] Test: cargo check + +## Network Detection +- [ ] Update get_network_info method +- [ ] Add enrich_network_interface_sysfs helper +- [ ] Test: cargo check + +## Final +- [ ] Add read_sysfs_file general helper +- [ ] Run full test suite: cargo test +- [ ] Test on real hardware +``` + +Good luck with your implementation! diff --git a/src/adapters/secondary/publisher/file.rs b/src/adapters/secondary/publisher/file.rs index f1ae32b..5af1bb2 100644 --- a/src/adapters/secondary/publisher/file.rs +++ b/src/adapters/secondary/publisher/file.rs @@ -221,6 +221,7 @@ mod tests { threads: 2, sockets: 1, speed: "3.0 GHz".to_string(), + ..Default::default() }, memory: crate::domain::MemoryInfo { total: "16GB".to_string(), diff --git a/src/adapters/secondary/publisher/http.rs b/src/adapters/secondary/publisher/http.rs index 2ed8364..d294a8b 100644 --- a/src/adapters/secondary/publisher/http.rs +++ b/src/adapters/secondary/publisher/http.rs @@ -211,6 +211,7 @@ mod tests { threads: 2, sockets: 1, speed: "3.0 GHz".to_string(), + ..Default::default() }, memory: crate::domain::MemoryInfo { total: "16GB".to_string(), diff --git a/src/adapters/secondary/system/linux.rs b/src/adapters/secondary/system/linux.rs index fb2513b..7270b9e 100644 --- a/src/adapters/secondary/system/linux.rs +++ b/src/adapters/secondary/system/linux.rs @@ -15,20 +15,69 @@ limitations under the License. */ //! Linux system information provider +//! +//! Implements `SystemInfoProvider` for Linux systems using: +//! - sysfs (`/sys`) for direct kernel data +//! - procfs (`/proc`) for process/system info +//! - Command execution for tools like lsblk, nvidia-smi +//! +//! # Platform Support +//! +//! - x86_64: Full support including raw-cpuid +//! - aarch64: Full support via sysfs (ARM servers, DGX Spark) +//! +//! # Detection Strategy +//! +//! Each hardware type uses multiple detection methods: +//! 1. Primary: sysfs (most reliable, always available) +//! 2. Secondary: Command output (lsblk, nvidia-smi, etc.) +//! 3. Fallback: sysinfo crate (cross-platform) use crate::domain::{ - combine_cpu_info, determine_memory_speed, determine_memory_type, parse_dmidecode_bios_info, - parse_dmidecode_chassis_info, parse_dmidecode_cpu, parse_dmidecode_memory, - parse_dmidecode_system_info, parse_free_output, parse_hostname_output, parse_ip_output, - parse_lsblk_output, parse_lscpu_output, BiosInfo, ChassisInfo, CpuInfo, GpuInfo, MemoryInfo, - MotherboardInfo, NetworkInfo, NumaNode, StorageInfo, SystemError, SystemInfo, + combine_cpu_info, + determine_memory_speed, + determine_memory_type, + parse_dmidecode_bios_info, + parse_dmidecode_chassis_info, + parse_dmidecode_cpu, + parse_dmidecode_memory, + parse_dmidecode_system_info, + parse_free_output, + parse_hostname_output, + parse_ip_output, + parse_lscpu_output, + BiosInfo, + ChassisInfo, + CpuInfo, + GpuDevice, + GpuInfo, + GpuVendor, + MemoryInfo, + MotherboardInfo, + NetworkInfo, + NetworkInterface, + NetworkInterfaceType, + NumaNode, + StorageDevice, + StorageInfo, + StorageType, + SystemError, + SystemInfo, }; + +use crate::domain::parsers::storage::{ + is_virtual_device, parse_lsblk_json, parse_sysfs_rotational, parse_sysfs_size, +}; + use crate::ports::{CommandExecutor, SystemCommand, SystemInfoProvider}; use async_trait::async_trait; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; +use std::fs; +use std::path::{Path, PathBuf}; + /// Linux system information provider using standard system commands pub struct LinuxSystemInfoProvider { command_executor: Arc, @@ -60,12 +109,330 @@ impl LinuxSystemInfoProvider { } missing } + + /// Read a sysfs file and return contents as String + fn read_sysfs_file(&self, path: &Path) -> Result { + fs::read_to_string(path) + } + + /// Detect storage devices via sysfs /sys/block. + async fn detect_storage_sysfs(&self) -> Result, SystemError> { + let mut devices = Vec::new(); + + let sys_block = Path::new("/sys/block"); + + if !sys_block.exists() { + return Err(SystemError::NotAvailable { + resource: "/sys/block".to_string(), + }); + } + + let entries = fs::read_dir(sys_block).map_err(|e| SystemError::IoErrorWithPath { + path: "/sys/block".to_string(), + message: e.to_string(), + })?; + + for entry in entries.flatten() { + let device_name = entry.file_name().to_string_lossy().to_string(); + + // Filter early to avoid needless I/O + if is_virtual_device(&device_name) { + log::trace!("Skipping virtual device: {}", device_name); + continue; + } + + let device_path = entry.path(); + + // If we can't get the size, skip the device + let size_path = device_path.join("size"); + let Ok(content) = self.read_sysfs_file(&size_path) else { + continue; + }; + let Ok(size_bytes) = parse_sysfs_size(&content) else { + log::trace!("Cannot parse size for {}: invalid format", device_name); + continue; + }; + + // Skip tiny devices (< 1GB) + const MIN_SIZE: u64 = 1_000_000_000; + if size_bytes < MIN_SIZE { + log::trace!("Skipping small device {}: {} bytes", device_name, size_bytes); + continue; + } + + // Rotational flag + let rotational_path = device_path.join("queue/rotational"); + let is_rotational = self + .read_sysfs_file(&rotational_path) + .map(|content| parse_sysfs_rotational(&content)) + .unwrap_or(false); + + // Determine device type + let device_type = StorageType::from_device(&device_name, is_rotational); + + // Read optional fields + let model = self + .read_sysfs_file(&device_path.join("device/model")) + .map(|s| s.trim().to_string()) + .unwrap_or_default(); + + let serial_number = self + .read_sysfs_file(&device_path.join("device/serial")) + .map(|s| s.trim().to_string()) + .ok() + .filter(|s| !s.is_empty()); + + let firmware_version = self + .read_sysfs_file(&device_path.join("device/firmware_rev")) + .map(|s| s.trim().to_string()) + .ok() + .filter(|s| !s.is_empty()); + + // Alternate paths for NVMe + let (serial_number, firmware_version) = if device_type == StorageType::Nvme { + self.read_nvme_sysfs_attrs(&device_name, serial_number, firmware_version) + } else { + (serial_number, firmware_version) + }; + + let interface = match &device_type { + StorageType::Nvme => "NVMe".to_string(), + StorageType::Emmc => "eMMC".to_string(), + StorageType::Hdd | StorageType::Ssd => "SATA".to_string(), + _ => "Unknown".to_string(), + }; + + let mut device = StorageDevice { + name: device_name.clone(), + device_path: format!("/dev/{}", device_name), + device_type: device_type.clone(), + type_: device_type.display_name().to_string(), + size_bytes, + model, + serial_number, + firmware_version, + interface, + is_rotational, + detection_method: "sysfs".to_string(), + ..Default::default() + }; + + device.calculate_size_fields(); + devices.push(device); + } + + Ok(devices) + } + + /// Read NVMe-specific sysfs attributes + fn read_nvme_sysfs_attrs( + &self, + device_name: &str, + existing_serial: Option, + existing_firmware: Option, + ) -> (Option, Option) { + // Extract controller name: "nvme0n1" -> "nvme0" + let controller = if device_name.starts_with("nvme") { + if let Some(pos) = device_name[4..].find('n') { + &device_name[..4 + pos] + } else { + device_name + } + } else { + device_name + }; + + let nvme_path = PathBuf::from("/sys/class/nvme").join(controller); + + let serial = existing_serial.or_else(|| { + self.read_sysfs_file(&nvme_path.join("serial")) + .map(|s| s.trim().to_string()) + .ok() + .filter(|s| !s.is_empty()) + }); + + let firmware = existing_firmware.or_else(|| { + self.read_sysfs_file(&nvme_path.join("firmware_rev")) + .map(|s| s.trim().to_string()) + .ok() + .filter(|s| !s.is_empty()) + }); + + (serial, firmware) + } + + /// Detect storage via lsblk command (JSON output) + async fn detect_storage_lsblk(&self) -> Result, SystemError> { + let cmd = SystemCommand::new("lsblk") + .args(&[ + "-J", + "-b", + "-d", + "-o", + "NAME,SIZE,TYPE,MODEL,SERIAL,ROTA,TRAN,WWN", + ]) + .timeout(Duration::from_secs(10)); + + let output = self.command_executor.execute(&cmd).await.map_err(|e| { + SystemError::CommandFailed { + command: "lsblk".to_string(), + exit_code: None, + stderr: e.to_string(), + } + })?; + + if !output.success { + return Err(SystemError::CommandFailed { + command: "lsblk".to_string(), + exit_code: output.exit_code, + stderr: output.stderr, + }); + } + + parse_lsblk_json(&output.stdout).map_err(SystemError::ParseError) + } + + /// Detect storage via sysinfo crate (cross-platform fallback) + fn detect_storage_sysinfo(&self) -> Result, SystemError> { + use sysinfo::Disks; + + let disks = Disks::new_with_refreshed_list(); + let mut devices = Vec::new(); + + for disk in disks.iter() { + let size_bytes = disk.total_space(); + + if size_bytes < 1_000_000_000 { + continue; + } + + let name = disk.name().to_string_lossy().to_string(); + let name = if name.is_empty() { + disk.mount_point().to_string_lossy().to_string() + } else { + name + }; + + let mut device = StorageDevice { + name, + size_bytes, + detection_method: "sysinfo".to_string(), + ..Default::default() + }; + + device.calculate_size_fields(); + devices.push(device); + } + Ok(devices) + } + + /// Merge storage info from secondary source into primary + fn merge_storage_info(&self, primary: &mut Vec, secondary: Vec) { + for sec_device in secondary { + if let Some(pri_device) = primary.iter_mut().find(|d| d.name == sec_device.name) { + // Fill in missing fields from secondary + pri_device.serial_number = pri_device.serial_number.take().or(sec_device.serial_number); + pri_device.firmware_version = pri_device.firmware_version.take().or(sec_device.firmware_version); + pri_device.wwn = pri_device.wwn.take().or(sec_device.wwn); + + if pri_device.model.is_empty() && !sec_device.model.is_empty() { + pri_device.model = sec_device.model; + } + } else { + // Device not in primary - add it + primary.push(sec_device); + } + } + } + + /// Enrich network interface with sysfs data + fn enrich_network_interface_sysfs(&self, iface: &mut NetworkInterface) { + let iface_path = PathBuf::from("/sys/class/net").join(&iface.name); + + if !iface_path.exists() { + return; + } + + // Operational state + iface.is_up = self + .read_sysfs_file(&iface_path.join("operstate")) + .map(|s| s.trim().to_lowercase() == "up") + .unwrap_or(false); + + // Speed (may be -1 if link is down) + if let Ok(speed_str) = self.read_sysfs_file(&iface_path.join("speed")) { + if let Ok(speed) = speed_str.trim().parse::() { + if speed > 0 { + iface.speed_mbps = Some(speed as u32); + iface.speed = Some(format!("{} Mbps", speed)); + } + } + } + + // MTU + if let Ok(mtu_str) = self.read_sysfs_file(&iface_path.join("mtu")) { + if let Ok(mtu) = mtu_str.trim().parse::() { + iface.mtu = mtu; + } + } + + // Carrier (link detected) + iface.carrier = self + .read_sysfs_file(&iface_path.join("carrier")) + .map(|s| s.trim() == "1") + .ok(); + + // Virtual interface detection + let device_path = iface_path.join("device"); + iface.is_virtual = !device_path.exists() + || iface.name.starts_with("lo") + || iface.name.starts_with("veth") + || iface.name.starts_with("br") + || iface.name.starts_with("docker") + || iface.name.starts_with("virbr"); + + // Driver information (only for physical interfaces) + if !iface.is_virtual { + let driver_link = device_path.join("driver"); + if let Ok(driver_path) = fs::read_link(&driver_link) { + if let Some(driver_name) = driver_path.file_name() { + let driver_str = driver_name.to_string_lossy().to_string(); + iface.driver = Some(driver_str.clone()); + + // Driver version + let version_path = + PathBuf::from("/sys/module").join(&driver_str).join("version"); + if let Ok(version) = self.read_sysfs_file(&version_path) { + iface.driver_version = Some(version.trim().to_string()); + } + } + } + } + + // Interface type + iface.interface_type = if iface.name == "lo" { + NetworkInterfaceType::Loopback + } else if iface.name.starts_with("br") || iface.name.starts_with("virbr") { + NetworkInterfaceType::Bridge + } else if iface.name.starts_with("veth") { + NetworkInterfaceType::Veth + } else if iface.name.starts_with("bond") { + NetworkInterfaceType::Bond + } else if iface.name.contains('.') { + NetworkInterfaceType::Vlan + } else if iface.name.starts_with("wl") { + NetworkInterfaceType::Wireless + } else if iface.name.starts_with("ib") { + NetworkInterfaceType::Infiniband + } else { + NetworkInterfaceType::Ethernet + }; + } } #[async_trait] impl SystemInfoProvider for LinuxSystemInfoProvider { async fn get_cpu_info(&self) -> Result { - // Get CPU info from lscpu let lscpu_cmd = SystemCommand::new("lscpu").timeout(Duration::from_secs(10)); let lscpu_output = self .command_executor @@ -80,7 +447,6 @@ impl SystemInfoProvider for LinuxSystemInfoProvider { let lscpu_info = parse_lscpu_output(&lscpu_output.stdout).map_err(SystemError::ParseError)?; - // Try to get additional info from dmidecode (may require sudo) let dmidecode_cmd = SystemCommand::new("dmidecode") .args(&["-t", "processor"]) .timeout(Duration::from_secs(10)); @@ -97,14 +463,13 @@ impl SystemInfoProvider for LinuxSystemInfoProvider { Ok(lscpu_info) } } - _ => Ok(lscpu_info), // Fall back to lscpu info + _ => Ok(lscpu_info), } } async fn get_memory_info(&self) -> Result { - // Get total memory from free command let free_cmd = SystemCommand::new("free") - .args(&["-b"]) // Show in bytes + .args(&["-b"]) .timeout(Duration::from_secs(5)); let free_output = self .command_executor @@ -119,7 +484,6 @@ impl SystemInfoProvider for LinuxSystemInfoProvider { let total_memory = parse_free_output(&free_output.stdout).map_err(SystemError::ParseError)?; - // Try to get detailed memory info from dmidecode let dmidecode_cmd = SystemCommand::new("dmidecode") .args(&["-t", "memory"]) .timeout(Duration::from_secs(10)); @@ -151,26 +515,45 @@ impl SystemInfoProvider for LinuxSystemInfoProvider { } async fn get_storage_info(&self) -> Result { - let lsblk_cmd = SystemCommand::new("lsblk") - .args(&["-d", "-o", "NAME,SIZE,TYPE"]) - .timeout(Duration::from_secs(10)); - let lsblk_output = self - .command_executor - .execute(&lsblk_cmd) - .await - .map_err(|e| SystemError::CommandFailed { - command: "lsblk".to_string(), - exit_code: None, - stderr: e.to_string(), - })?; + let mut devices = Vec::new(); - let devices = parse_lsblk_output(&lsblk_output.stdout).map_err(SystemError::ParseError)?; + // Try sysfs first + if let Ok(sysfs_devices) = self.detect_storage_sysfs().await { + log::debug!("sysfs detected {} storage devices", sysfs_devices.len()); + devices = sysfs_devices; + } else { + log::warn!("sysfs storage detection failed, trying next method"); + } + // Enrich with lsblk + if let Ok(lsblk_devices) = self.detect_storage_lsblk().await { + log::debug!("lsblk found {} devices for additional info", lsblk_devices.len()); + self.merge_storage_info(&mut devices, lsblk_devices); + } + + // Fallback to sysinfo + if devices.is_empty() { + log::warn!("No devices from sysfs/lsblk, trying sysinfo as fallback"); + if let Ok(sysinfo_devices) = self.detect_storage_sysinfo() { + devices = sysinfo_devices; + } + } + + // Filter virtual devices and sort + devices.retain(|d| d.device_type != StorageType::Virtual); + + for device in &mut devices { + if device.size_gb == 0.0 && device.size_bytes > 0 { + device.calculate_size_fields(); + } + device.set_device_path(); + } + + devices.sort_by(|a, b| a.name.cmp(&b.name)); Ok(StorageInfo { devices }) } async fn get_gpu_info(&self) -> Result { - // Try nvidia-smi first let nvidia_cmd = SystemCommand::new("nvidia-smi") .args(&[ "--query-gpu=index,name,uuid,memory.total", @@ -182,25 +565,29 @@ impl SystemInfoProvider for LinuxSystemInfoProvider { if let Ok(nvidia_output) = self.command_executor.execute(&nvidia_cmd).await { if nvidia_output.success { - // Parse NVIDIA GPU info for (index, line) in nvidia_output.stdout.lines().enumerate() { let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect(); if parts.len() >= 4 { - devices.push(crate::domain::GpuDevice { + let memory_mb: u64 = parts[3].parse().unwrap_or(0); + devices.push(GpuDevice { index: index as u32, name: parts[1].to_string(), uuid: parts[2].to_string(), memory: format!("{} MB", parts[3]), - pci_id: "Unknown".to_string(), + memory_total_mb: memory_mb, + pci_id: String::new(), vendor: "NVIDIA".to_string(), + vendor_enum: GpuVendor::Nvidia, numa_node: None, + detection_method: "nvidia-smi".to_string(), + ..Default::default() }); } } } } - // Fall back to lspci for basic GPU detection + // Fallback to lspci if devices.is_empty() { let lspci_cmd = SystemCommand::new("lspci") .args(&["-nn"]) @@ -210,16 +597,20 @@ impl SystemInfoProvider for LinuxSystemInfoProvider { if lspci_output.success { let mut gpu_index = 0; for line in lspci_output.stdout.lines() { - if line.to_lowercase().contains("vga") || line.to_lowercase().contains("3d") + if line.to_lowercase().contains("vga") + || line.to_lowercase().contains("3d") { - devices.push(crate::domain::GpuDevice { + devices.push(GpuDevice { index: gpu_index, name: line.to_string(), uuid: format!("pci-gpu-{gpu_index}"), memory: "Unknown".to_string(), - pci_id: "Unknown".to_string(), + pci_id: String::new(), vendor: "Unknown".to_string(), + vendor_enum: GpuVendor::Unknown, numa_node: None, + detection_method: "lspci".to_string(), + ..Default::default() }); gpu_index += 1; } @@ -243,11 +634,17 @@ impl SystemInfoProvider for LinuxSystemInfoProvider { } })?; - let interfaces = parse_ip_output(&ip_output.stdout).map_err(SystemError::ParseError)?; + let mut interfaces = + parse_ip_output(&ip_output.stdout).map_err(SystemError::ParseError)?; + + // Enrich with sysfs data + for iface in &mut interfaces { + self.enrich_network_interface_sysfs(iface); + } Ok(NetworkInfo { interfaces, - infiniband: None, // TODO: Add infiniband detection + infiniband: None, }) } @@ -299,8 +696,6 @@ impl SystemInfoProvider for LinuxSystemInfoProvider { stderr: e.to_string(), })?; - // Parse motherboard info (simplified) - // TODO: Parse _dmidecode_output.stdout to extract actual values Ok(MotherboardInfo { manufacturer: "Unknown".to_string(), product_name: "Unknown".to_string(), @@ -330,7 +725,6 @@ impl SystemInfoProvider for LinuxSystemInfoProvider { } async fn get_numa_topology(&self) -> Result, SystemError> { - // Simplified NUMA topology - in real implementation this would be more comprehensive Ok(HashMap::new()) } @@ -380,7 +774,6 @@ impl SystemInfoProvider for LinuxSystemInfoProvider { let mut filesystems = Vec::new(); for line in df_output.stdout.lines().skip(1) { - // Skip header if !line.trim().is_empty() { filesystems.push(line.to_string()); } diff --git a/src/adapters/secondary/system/macos.rs b/src/adapters/secondary/system/macos.rs index 5db51e2..66eb308 100644 --- a/src/adapters/secondary/system/macos.rs +++ b/src/adapters/secondary/system/macos.rs @@ -161,6 +161,7 @@ impl SystemInfoProvider for MacOSSystemInfoProvider { pci_id: "Apple Fabric (Integrated)".to_string(), vendor: "Apple".to_string(), numa_node: None, + ..Default::default() }); gpu_index += 1; } @@ -176,6 +177,7 @@ impl SystemInfoProvider for MacOSSystemInfoProvider { pci_id: "Apple Fabric (Integrated)".to_string(), vendor: "Apple".to_string(), numa_node: None, + ..Default::default() }); } diff --git a/src/domain/entities.rs b/src/domain/entities.rs index cc32abf..e0b5d91 100644 --- a/src/domain/entities.rs +++ b/src/domain/entities.rs @@ -159,6 +159,122 @@ pub struct HardwareInfo { pub gpus: GpuInfo, } +/// CPU information +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CpuInfo { + /// CPU model name + pub model: String, + /// Number of cores per socket + pub cores: u32, + /// Number of threads per core + pub threads: u32, + /// Number of sockets + pub sockets: u32, + /// CPU speed as string + pub speed: String, + /// CPU vendor (e.g., "GenuineIntel", "AuthenticAMD") + #[serde(default)] + pub vendor: String, + /// CPU architecture + #[serde(default)] + pub architecture: String, + /// CPU frequency in MHz + #[serde(default)] + pub frequency_mhz: u32, + /// Minimum frequency in MHz + #[serde(default)] + pub frequency_min_mhz: Option, + /// Maximum frequency in MHz + #[serde(default)] + pub frequency_max_mhz: Option, + /// L1 data cache size in KB + #[serde(default)] + pub cache_l1d_kb: Option, + /// L1 instruction cache size in KB + #[serde(default)] + pub cache_l1i_kb: Option, + /// L2 cache size in KB + #[serde(default)] + pub cache_l2_kb: Option, + /// L3 cache size in KB + #[serde(default)] + pub cache_l3_kb: Option, + /// CPU flags/features + #[serde(default)] + pub flags: Vec, + /// Microarchitecture (e.g., "Zen 3", "Ice Lake") + #[serde(default)] + pub microarchitecture: Option, + /// Detailed cache information + #[serde(default)] + pub caches: Vec, + /// Detection methods used + #[serde(default)] + pub detection_methods: Vec, +} + +impl Default for CpuInfo { + fn default() -> Self { + Self { + model: String::new(), + cores: 0, + threads: 0, + sockets: 0, + speed: String::new(), + vendor: String::new(), + architecture: String::new(), + frequency_mhz: 0, + frequency_min_mhz: None, + frequency_max_mhz: None, + cache_l1d_kb: None, + cache_l1i_kb: None, + cache_l2_kb: None, + cache_l3_kb: None, + flags: Vec::new(), + microarchitecture: None, + caches: Vec::new(), + detection_methods: Vec::new(), + } + } +} + +impl CpuInfo { + /// Set speed string from frequency_mhz + pub fn set_speed_string(&mut self) { + if self.frequency_mhz > 0 { + if self.frequency_mhz >= 1000 { + self.speed = format!("{:.2} GHz", self.frequency_mhz as f64 / 1000.0); + } else { + self.speed = format!("{} MHz", self.frequency_mhz); + } + } + } + + /// Calculate total cores and threads + pub fn calculate_totals(&mut self) { + // These are typically already set correctly from parsing + } +} + +/// CPU cache information +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct CpuCacheInfo { + /// Cache level (1, 2, 3, etc.) + pub level: u8, + /// Cache type ("Data", "Instruction", "Unified") + pub cache_type: String, + /// Size in KB + pub size_kb: u32, + /// Ways of associativity + pub ways_of_associativity: Option, + /// Cache line size in bytes + pub line_size_bytes: Option, + /// Number of sets + pub sets: Option, + /// Whether this cache is shared between cores + pub shared: Option, +} + /// Memory information #[derive(Debug, Serialize, Deserialize, Clone)] pub struct MemoryInfo { @@ -187,9 +303,6 @@ pub struct MemoryModule { pub manufacturer: String, /// Serial number pub serial: String, - pub part_number: Option, - pub rank: Option, - pub configured_voltage: Option, } /// Storage information @@ -199,55 +312,141 @@ pub struct StorageInfo { pub devices: Vec, } -/// GPU device information -/// -/// Represents a discrete or integrated GPU detected in the system. -/// Memory values are provided in megabytes as unsigned integers for -/// reliable parsing by CMDB consumers. -/// -/// # Detection Methods -/// -/// GPUs are detected using multiple methods in priority order: -/// 1. NVML (NVIDIA Management Library) - most accurate for NVIDIA GPUs -/// 2. nvidia-smi command - fallback for NVIDIA when NVML unavailable -/// 3. ROCm SMI - AMD GPU detection -/// 4. sysfs /sys/class/drm - Linux DRM subsystem -/// 5. lspci - PCI device enumeration -/// 6. sysinfo crate - cross-platform fallback -/// -/// # References -/// -/// - [NVIDIA NVML Documentation](https://developer.nvidia.com/nvidia-management-library-nvml) -/// - [Linux DRM Subsystem](https://www.kernel.org/doc/html/latest/gpu/drm-uapi.html) -/// - [PCI ID Database](https://pci-ids.ucw.cz/) -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct GpuDevice { - /// GPU index (0-based) - pub index: u32, - - /// GPU product name - pub name: String, +/// Storage type classification +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub enum StorageType { + /// NVMe SSD + Nvme, + /// SATA SSD + Ssd, + /// Hard Disk Drive + Hdd, + /// eMMC storage (common on ARM) + Emmc, + /// Virtual device (should be filtered) + Virtual, + /// Unknown type + Unknown, +} - /// GPU UUUD - pub uuid: String, +impl StorageType { + /// Determine storage type from device name and rotational flag + pub fn from_device(name: &str, is_rotational: bool) -> Self { + if name.starts_with("nvme") { + StorageType::Nvme + } else if name.starts_with("mmcblk") { + StorageType::Emmc + } else if name.starts_with("loop") || name.starts_with("ram") || name.starts_with("dm-") { + StorageType::Virtual + } else if is_rotational { + StorageType::Hdd + } else { + StorageType::Ssd + } + } - /// Vendor name - pub vendor: String, + /// Get display name for the storage type + pub fn display_name(&self) -> &'static str { + match self { + StorageType::Nvme => "NVMe SSD", + StorageType::Ssd => "SSD", + StorageType::Hdd => "HDD", + StorageType::Emmc => "eMMC", + StorageType::Virtual => "Virtual", + StorageType::Unknown => "Unknown", + } + } +} - /// Driver Version - pub driver_version: Option, +impl Default for StorageType { + fn default() -> Self { + StorageType::Unknown + } +} - /// CUDA compute capability for Nvidia gpus - pub compute_capability: Option, +/// Storage device information +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct StorageDevice { + /// Device name (e.g., "sda", "nvme0n1") + pub name: String, + /// Device path (e.g., "/dev/sda") + #[serde(default)] + pub device_path: String, + /// Device type enum + #[serde(default)] + pub device_type: StorageType, + /// Device type as string (for backward compatibility) + pub type_: String, + /// Device size as human-readable string + pub size: String, + /// Device size in bytes + #[serde(default)] + pub size_bytes: u64, + /// Device size in GB + #[serde(default)] + pub size_gb: f64, + /// Device model + pub model: String, + /// Serial number + #[serde(default)] + pub serial_number: Option, + /// Firmware version + #[serde(default)] + pub firmware_version: Option, + /// World Wide Name + #[serde(default)] + pub wwn: Option, + /// Interface type (e.g., "NVMe", "SATA", "SAS") + #[serde(default)] + pub interface: String, + /// Whether this is a rotational device (HDD) + #[serde(default)] + pub is_rotational: bool, + /// Detection method used + #[serde(default)] + pub detection_method: String, +} - /// GPU architecturr (Hopper, Ada LoveLace) - pub architecture: Option, +impl Default for StorageDevice { + fn default() -> Self { + Self { + name: String::new(), + device_path: String::new(), + device_type: StorageType::Unknown, + type_: String::new(), + size: String::new(), + size_bytes: 0, + size_gb: 0.0, + model: String::new(), + serial_number: None, + firmware_version: None, + wwn: None, + interface: String::new(), + is_rotational: false, + detection_method: String::new(), + } + } +} - /// NUMA node affiniity (-1 if not applicable) - pub numa_node: Option, +impl StorageDevice { + /// Calculate size_gb and size string from size_bytes + pub fn calculate_size_fields(&mut self) { + if self.size_bytes > 0 { + self.size_gb = self.size_bytes as f64 / (1024.0 * 1024.0 * 1024.0); + if self.size_gb >= 1000.0 { + self.size = format!("{:.2} TB", self.size_gb / 1024.0); + } else { + self.size = format!("{:.2} GB", self.size_gb); + } + } + } - /// Detection method used to dsicover this GPU - pub detection_method: String, + /// Set device path from name if not already set + pub fn set_device_path(&mut self) { + if self.device_path.is_empty() && !self.name.is_empty() { + self.device_path = format!("/dev/{}", self.name); + } + } } /// GPU information @@ -256,6 +455,125 @@ pub struct GpuInfo { /// List of GPU devices pub devices: Vec, } + +/// GPU vendor classification +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub enum GpuVendor { + /// NVIDIA GPU + Nvidia, + /// AMD GPU + Amd, + /// Intel GPU + Intel, + /// Apple GPU (Apple Silicon) + Apple, + /// Unknown vendor + Unknown, +} + +impl GpuVendor { + /// Determine vendor from PCI vendor ID + pub fn from_pci_vendor(vendor_id: &str) -> Self { + match vendor_id.to_lowercase().as_str() { + "10de" => GpuVendor::Nvidia, + "1002" => GpuVendor::Amd, + "8086" => GpuVendor::Intel, + _ => GpuVendor::Unknown, + } + } + + /// Get vendor name string + pub fn name(&self) -> &'static str { + match self { + GpuVendor::Nvidia => "NVIDIA", + GpuVendor::Amd => "AMD", + GpuVendor::Intel => "Intel", + GpuVendor::Apple => "Apple", + GpuVendor::Unknown => "Unknown", + } + } +} + +impl Default for GpuVendor { + fn default() -> Self { + GpuVendor::Unknown + } +} + +/// GPU device information +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GpuDevice { + /// GPU index + pub index: u32, + /// GPU name + pub name: String, + /// GPU UUID + pub uuid: String, + /// Total GPU memory as string (for backward compatibility) + pub memory: String, + /// Total GPU memory in MB + #[serde(default)] + pub memory_total_mb: u64, + /// Free GPU memory in MB + #[serde(default)] + pub memory_free_mb: Option, + /// PCI ID (vendor:device) or Apple Fabric for Apple Silicon + pub pci_id: String, + /// PCI bus ID (e.g., "0000:01:00.0") + #[serde(default)] + pub pci_bus_id: Option, + /// Vendor name (for backward compatibility) + pub vendor: String, + /// Vendor classification enum + #[serde(default)] + pub vendor_enum: GpuVendor, + /// NUMA node + pub numa_node: Option, + /// Driver version + #[serde(default)] + pub driver_version: Option, + /// Compute capability (NVIDIA specific) + #[serde(default)] + pub compute_capability: Option, + /// Detection method used + #[serde(default)] + pub detection_method: String, +} + +impl Default for GpuDevice { + fn default() -> Self { + Self { + index: 0, + name: String::new(), + uuid: String::new(), + memory: String::new(), + memory_total_mb: 0, + memory_free_mb: None, + pci_id: String::new(), + pci_bus_id: None, + vendor: String::new(), + vendor_enum: GpuVendor::Unknown, + numa_node: None, + driver_version: None, + compute_capability: None, + detection_method: String::new(), + } + } +} + +impl GpuDevice { + /// Set memory string from memory_total_mb + pub fn set_memory_string(&mut self) { + if self.memory_total_mb > 0 { + if self.memory_total_mb >= 1024 { + self.memory = format!("{:.1} GB", self.memory_total_mb as f64 / 1024.0); + } else { + self.memory = format!("{} MB", self.memory_total_mb); + } + } + } +} + /// Network information #[derive(Debug, Serialize, Deserialize, Clone)] pub struct NetworkInfo { @@ -265,30 +583,39 @@ pub struct NetworkInfo { pub infiniband: Option, } -/// Storage device type classification -/// -/// # References -/// -/// - [Linux Block Device Documentation](https://www.kernel.org/doc/html/latest/block/index.html) -/// - [NVMe Specification](https://nvmexpress.org/specifications/) -#[derive(Debug, Serialize, Deserialize, Clone)] -pub enum StorageType { - /// NVMe ssd - Nvme, - - /// SATA/SAS ssd - Ssd, - - /// Hard disk (rotational) - Hdd, - - /// Embedded MMC Storage - Emmc, - - /// Unknown or unclassified storage type +/// Network interface type classification +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub enum NetworkInterfaceType { + /// Physical Ethernet interface + Ethernet, + /// Wireless interface + Wireless, + /// Loopback interface + Loopback, + /// Bridge interface + Bridge, + /// VLAN interface + Vlan, + /// Bond/LAG interface + Bond, + /// Virtual Ethernet (veth pair) + Veth, + /// TUN/TAP interface + TunTap, + /// InfiniBand interface + Infiniband, + /// Macvlan interface + Macvlan, + /// Unknown type Unknown, } +impl Default for NetworkInterfaceType { + fn default() -> Self { + NetworkInterfaceType::Unknown + } +} + /// Network interface information #[derive(Debug, Serialize, Deserialize, Clone)] pub struct NetworkInterface { @@ -300,10 +627,16 @@ pub struct NetworkInterface { pub ip: String, /// IP prefix pub prefix: String, - /// Interface speed + /// Interface speed as string (e.g., "1000 Mbps") pub speed: Option, - /// Interface type + /// Interface speed in Mbps (numeric) + #[serde(default)] + pub speed_mbps: Option, + /// Interface type as string (for backward compatibility) pub type_: String, + /// Interface type classification + #[serde(default)] + pub interface_type: NetworkInterfaceType, /// Vendor pub vendor: String, /// Model @@ -312,122 +645,53 @@ pub struct NetworkInterface { pub pci_id: String, /// NUMA node pub numa_node: Option, - pub driver: Option, - pub driver_version: Option, - pub firmware_version: Option, - pub mtu: u32, - pub is_up: bool, - pub is_virtual: bool, + /// Kernel driver in use + #[serde(default)] + pub driver: Option, + /// Driver version + #[serde(default)] + pub driver_version: Option, + /// Maximum Transmission Unit in bytes + #[serde(default = "default_mtu")] + pub mtu: u32, + /// Whether the interface is operationally up + #[serde(default)] + pub is_up: bool, + /// Whether this is a virtual interface + #[serde(default)] + pub is_virtual: bool, + /// Link detected (carrier present) + #[serde(default)] + pub carrier: Option, } -/// Storage device information -/// -/// # Detection Methods -/// -/// Storage devices are detected using multiple methods in priority order: -/// 1. sysfs /sys/block - direct kernel interface (Linux) -/// 2. lsblk command - block device listing -/// 3. sysinfo crate - cross-platform fallback -/// 4. diskutil (macOS) -/// -/// # References -/// -/// - [Linux sysfs Block Devices](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-block) -/// - [SMART Attributes](https://en.wikipedia.org/wiki/S.M.A.R.T.) -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct StorageDevice { - /// Device name (nvme0n1, sda etc..,) - pub name: String, - - /// Device type classification - pub device_type: StorageType, - - /// Legacy type field - #[deprecated(since = "0.2.0", note = "Use device_type instead")] - #[serde(skip_serializing_if = "Option::is_none")] - pub type_: Option, - - /// Device size in bytes - pub size_bytes: u64, - - /// Device size in gigabyes - pub size_gb: f64, - - /// Legacy size field as string (deprecated) - #[deprecated(since = "0.2.0", note = "Use size_bytes or size_gb instead")] - #[serde(skip_serializing_if = "Option::is_none")] - pub size: Option, - - /// Device model name - pub model: String, - - /// Device serial number (may require elevated privileges) - pub serial_number: Option, - - /// Device firmware version - pub firmware_version: Option, - - /// Interface type (e.g., "NVMe", "SATA", "SAS", "eMMC") - pub interface: String, - - /// Whether the device is rotational (true = HDD, false = SSD/NVMe) - pub is_rotational: bool, - - /// WWN (World Wide Name) if available - pub wwn: Option, - - /// Detection method used - pub detection_method: String, +fn default_mtu() -> u32 { + 1500 } -/// # Detection Methods -/// -/// CPU information is gathered from multiple sources: -/// 1. sysfs /sys/devices/system/cpu - frequency and cache (Linux) -/// 2. /proc/cpuinfo - model and features (Linux) -/// 3. raw-cpuid crate - x86 CPUID instruction -/// 4. lscpu command - topology information -/// 5. dmidecode - SMBIOS data (requires privileges) -/// 6. sysinfo crate - cross-platform fallback -/// -/// # References -/// -/// - [Linux CPU sysfs Interface](https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.rst) -/// - [Intel CPUID Reference](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) -/// - [ARM CPU Identification](https://developer.arm.com/documentation/ddi0487/latest) -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CpuInfo { - /// Cpu Model (AMD, Intel) - pub model: String, - - /// CPu Vendor - pub vendor: String, - - /// Number of phyiscal cores per socket - pub cores: u32, - - /// Number of threads per core - pub threads: u32, - - /// Number of CPU sockets - pub sockets: u32, - - /// CPU frequencies in MHz (uccrent or max) - pub frequency_mhz: u32, - - /// Legacy speed field as string (deprecated) - #[deprecated(since = "0.2.0", note = "Use frequency_mhz instead")] - #[serde(skip_serializing_if = "Option::is_none")] - pub speed: Option, - - /// CPU architecture - pub arhitecture: String, - - /// LI data cache size in kilobytes (per core) - pub cache_l1d_kb: Option, - - /// L1 instruction cache size in kilobytes (per core) - pub cache_l1li_kb: Option, +impl Default for NetworkInterface { + fn default() -> Self { + Self { + name: String::new(), + mac: String::new(), + ip: String::new(), + prefix: String::new(), + speed: None, + speed_mbps: None, + type_: String::new(), + interface_type: NetworkInterfaceType::Unknown, + vendor: String::new(), + model: String::new(), + pci_id: String::new(), + numa_node: None, + driver: None, + driver_version: None, + mtu: 1500, + is_up: false, + is_virtual: false, + carrier: None, + } + } } /// Infiniband information diff --git a/src/domain/errors.rs b/src/domain/errors.rs index ae52ad5..10aa1aa 100644 --- a/src/domain/errors.rs +++ b/src/domain/errors.rs @@ -139,12 +139,21 @@ pub enum SystemError { CommandNotFound(String), /// Permission denied PermissionDenied(String), - /// I/O operation failed + /// I/O operation failed (simple) IoError(String), + /// I/O operation failed (with path context) + IoErrorWithPath { + path: String, + message: String, + }, /// Parsing error ParseError(String), /// Timeout Timeout(String), + /// Resource not available + NotAvailable { + resource: String, + }, } impl fmt::Display for SystemError { @@ -167,8 +176,14 @@ impl fmt::Display for SystemError { SystemError::CommandNotFound(cmd) => write!(f, "Command not found: {cmd}"), SystemError::PermissionDenied(msg) => write!(f, "Permission denied: {msg}"), SystemError::IoError(msg) => write!(f, "I/O error: {msg}"), + SystemError::IoErrorWithPath { path, message } => { + write!(f, "I/O error at '{}': {}", path, message) + } SystemError::ParseError(msg) => write!(f, "Parse error: {msg}"), SystemError::Timeout(msg) => write!(f, "Timeout: {msg}"), + SystemError::NotAvailable { resource } => { + write!(f, "Resource not available: {}", resource) + } } } } @@ -189,8 +204,14 @@ impl From for DomainError { SystemError::IoError(msg) => { DomainError::SystemInfoUnavailable(format!("I/O error: {msg}")) } + SystemError::IoErrorWithPath { path, message } => { + DomainError::SystemInfoUnavailable(format!("I/O error at '{}': {}", path, message)) + } SystemError::ParseError(msg) => DomainError::ParsingFailed(msg), SystemError::Timeout(msg) => DomainError::Timeout(msg), + SystemError::NotAvailable { resource } => { + DomainError::SystemInfoUnavailable(format!("Resource not available: {}", resource)) + } } } } diff --git a/src/domain/legacy_compat.rs b/src/domain/legacy_compat.rs index 7407dc6..21fb096 100644 --- a/src/domain/legacy_compat.rs +++ b/src/domain/legacy_compat.rs @@ -258,6 +258,7 @@ impl From for new::CpuInfo { threads: legacy.threads, sockets: legacy.sockets, speed: legacy.speed, + ..Default::default() } } } @@ -342,9 +343,10 @@ impl From for new::StorageDevice { fn from(legacy: crate::StorageDevice) -> Self { new::StorageDevice { name: legacy.name, - type_: legacy.type_, + type_: legacy.type_.clone(), size: legacy.size, model: legacy.model, + ..Default::default() } } } @@ -386,6 +388,7 @@ impl From for new::GpuDevice { pci_id: legacy.pci_id, vendor: legacy.vendor, numa_node: legacy.numa_node, + ..Default::default() } } } @@ -435,6 +438,7 @@ impl From for new::NetworkInterface { model: legacy.model, pci_id: legacy.pci_id, numa_node: legacy.numa_node, + ..Default::default() } } } diff --git a/src/domain/parsers/cpu.rs b/src/domain/parsers/cpu.rs index e39ab83..591d292 100644 --- a/src/domain/parsers/cpu.rs +++ b/src/domain/parsers/cpu.rs @@ -26,6 +26,99 @@ lazy_static! { static ref CORE_COUNT_RE: Regex = Regex::new(r"(\d+)").unwrap(); } +/// Parse sysfs frequency file (kHz to MHz) +/// +/// # Arguments +/// +/// * `content` - Content of frequency file (value in kHz) +/// +/// # Returns +/// +/// Frequency in MHz. +pub fn parse_sysfs_freq_khz(content: &str) -> Result { + let khz: u64 = content + .trim() + .parse() + .map_err(|e| format!("Failed to parse frequency: {}", e))?; + Ok((khz / 1000) as u32) +} + +/// Parse sysfs cache size (e.g., "32K" -> 32) +/// +/// # Arguments +/// +/// * `content` - Content of cache size file (e.g., "32K", "256K", "16M") +/// +/// # Returns +/// +/// Size in KB. +pub fn parse_sysfs_cache_size(content: &str) -> Result { + let trimmed = content.trim(); + + if trimmed.ends_with('K') || trimmed.ends_with('k') { + let value: u32 = trimmed[..trimmed.len() - 1] + .parse() + .map_err(|e| format!("Failed to parse cache size: {}", e))?; + Ok(value) + } else if trimmed.ends_with('M') || trimmed.ends_with('m') { + let value: u32 = trimmed[..trimmed.len() - 1] + .parse() + .map_err(|e| format!("Failed to parse cache size: {}", e))?; + Ok(value * 1024) + } else { + // Assume bytes, convert to KB + let value: u32 = trimmed + .parse() + .map_err(|e| format!("Failed to parse cache size: {}", e))?; + Ok(value / 1024) + } +} + +/// Parse /proc/cpuinfo +/// +/// Extracts vendor, flags, and other CPU information. +/// +/// # Arguments +/// +/// * `content` - Content of /proc/cpuinfo +pub fn parse_proc_cpuinfo(content: &str) -> Result { + let mut cpu_info = CpuInfo::default(); + + for line in content.lines() { + if let Some((key, value)) = line.split_once(':') { + let key = key.trim(); + let value = value.trim(); + + match key { + "vendor_id" => cpu_info.vendor = value.to_string(), + "model name" => { + if cpu_info.model.is_empty() { + cpu_info.model = value.to_string(); + } + } + "flags" | "Features" => { + cpu_info.flags = value.split_whitespace().map(|s| s.to_string()).collect(); + } + "CPU implementer" => { + // ARM CPU implementer code + if cpu_info.vendor.is_empty() { + cpu_info.vendor = match value { + "0x41" => "ARM".to_string(), + "0x4e" => "NVIDIA".to_string(), + "0x51" => "Qualcomm".to_string(), + "0x61" => "Apple".to_string(), + _ => value.to_string(), + }; + } + } + _ => {} + } + } + } + + Ok(cpu_info) +} + /// Parse CPU information from Linux lscpu output /// /// # Arguments @@ -83,6 +176,7 @@ pub fn parse_lscpu_output(lscpu_output: &str) -> Result { threads, sockets, speed, + ..Default::default() }) } @@ -116,6 +210,7 @@ pub fn parse_dmidecode_cpu(dmidecode_output: &str) -> Result { threads, sockets: 1, // dmidecode typically shows per-socket info speed: clean_value(&speed), + ..Default::default() }) } @@ -175,6 +270,7 @@ pub fn parse_macos_cpu_info(system_profiler_output: &str) -> Result CpuInfo { } else { secondary.speed }, + ..Default::default() } } @@ -328,6 +425,7 @@ CPU MHz: 2300.000"#; threads: 2, sockets: 1, speed: "Unknown".to_string(), + ..Default::default() }; let secondary = CpuInfo { @@ -336,6 +434,7 @@ CPU MHz: 2300.000"#; threads: 0, sockets: 0, speed: "2.3 GHz".to_string(), + ..Default::default() }; let combined = combine_cpu_info(primary, secondary); @@ -352,6 +451,7 @@ CPU MHz: 2300.000"#; threads: 2, sockets: 1, speed: "2.3 GHz".to_string(), + ..Default::default() }; let topology = create_cpu_topology(&cpu_info, Some(1)); diff --git a/src/domain/parsers/gpu.rs b/src/domain/parsers/gpu.rs new file mode 100644 index 0000000..1447610 --- /dev/null +++ b/src/domain/parsers/gpu.rs @@ -0,0 +1,219 @@ +/* +Copyright 2024 San Francisco Compute Company + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +//! GPU information parsing functions + +use crate::domain::{GpuDevice, GpuVendor}; + +/// Parse nvidia-smi CSV output +/// +/// Expected format from command: +/// `nvidia-smi --query-gpu=index,name,uuid,memory.total,memory.free,pci.bus_id,driver_version,compute_cap --format=csv,noheader,nounits` +/// +/// # Arguments +/// +/// * `output` - CSV output from nvidia-smi +/// +/// # Returns +/// +/// List of GPU devices. +pub fn parse_nvidia_smi_output(output: &str) -> Result, String> { + let mut devices = Vec::new(); + + for line in output.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + + let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect(); + if parts.len() < 4 { + continue; + } + + let index: u32 = parts[0].parse().unwrap_or(devices.len() as u32); + let name = parts[1].to_string(); + let uuid = parts[2].to_string(); + let memory_total_mb: u64 = parts[3].parse().unwrap_or(0); + + let memory_free_mb = if parts.len() > 4 { + parts[4].parse().ok() + } else { + None + }; + + let pci_bus_id = if parts.len() > 5 { + Some(parts[5].to_string()) + } else { + None + }; + + let driver_version = if parts.len() > 6 && !parts[6].is_empty() { + Some(parts[6].to_string()) + } else { + None + }; + + let compute_capability = if parts.len() > 7 && !parts[7].is_empty() { + Some(parts[7].to_string()) + } else { + None + }; + + let mut device = GpuDevice { + index, + name, + uuid, + memory_total_mb, + memory_free_mb, + pci_bus_id, + vendor: "NVIDIA".to_string(), + vendor_enum: GpuVendor::Nvidia, + driver_version, + compute_capability, + detection_method: "nvidia-smi".to_string(), + ..Default::default() + }; + + device.set_memory_string(); + devices.push(device); + } + + Ok(devices) +} + +/// Parse lspci output for GPU devices +/// +/// Expected command: `lspci -nn` +/// +/// # Arguments +/// +/// * `output` - Output from lspci -nn +pub fn parse_lspci_gpu_output(output: &str) -> Result, String> { + let mut devices = Vec::new(); + let mut gpu_index = 0; + + for line in output.lines() { + let line_lower = line.to_lowercase(); + + // Look for VGA compatible or 3D controller + if !line_lower.contains("vga") && !line_lower.contains("3d") { + continue; + } + + // Extract PCI ID from brackets like [10de:2204] + let pci_id = extract_pci_id(line); + + // Determine vendor from PCI ID + let (vendor_enum, vendor_name) = if let Some(ref pci) = pci_id { + let vendor_id = pci.split(':').next().unwrap_or(""); + let vendor = GpuVendor::from_pci_vendor(vendor_id); + (vendor.clone(), vendor.name().to_string()) + } else { + (GpuVendor::Unknown, "Unknown".to_string()) + }; + + // Extract name (everything after the colon and space) + let name = line + .split_once(':') + .map(|(_, rest)| rest.trim()) + .unwrap_or(line) + .to_string(); + + let device = GpuDevice { + index: gpu_index, + name, + uuid: format!("lspci-gpu-{}", gpu_index), + pci_id: pci_id.clone().unwrap_or_default(), + vendor: vendor_name, + vendor_enum, + detection_method: "lspci".to_string(), + ..Default::default() + }; + + devices.push(device); + gpu_index += 1; + } + + Ok(devices) +} + +/// Extract PCI vendor:device ID from lspci output line +/// +/// Looks for pattern like [10de:2204] - must be 4 hex chars : 4 hex chars +fn extract_pci_id(line: &str) -> Option { + // Find all bracket patterns and look for PCI ID format [xxxx:yyyy] + let mut search_start = 0; + while let Some(start) = line[search_start..].find('[') { + let abs_start = search_start + start; + if let Some(end) = line[abs_start..].find(']') { + let bracket_content = &line[abs_start + 1..abs_start + end]; + + // Check if it looks like a PCI ID (4 hex chars : 4 hex chars) + if bracket_content.len() == 9 && bracket_content.chars().nth(4) == Some(':') { + // Verify it's all hex chars + let parts: Vec<&str> = bracket_content.split(':').collect(); + if parts.len() == 2 + && parts[0].len() == 4 + && parts[1].len() == 4 + && parts[0].chars().all(|c| c.is_ascii_hexdigit()) + && parts[1].chars().all(|c| c.is_ascii_hexdigit()) + { + return Some(bracket_content.to_string()); + } + } + search_start = abs_start + end + 1; + } else { + break; + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_nvidia_smi_output() { + let output = "0, NVIDIA GeForce RTX 3090, GPU-12345678-1234-1234-1234-123456789012, 24576, 24000, 00000000:01:00.0, 535.129.03, 8.6"; + let devices = parse_nvidia_smi_output(output).unwrap(); + + assert_eq!(devices.len(), 1); + assert_eq!(devices[0].name, "NVIDIA GeForce RTX 3090"); + assert_eq!(devices[0].memory_total_mb, 24576); + assert_eq!(devices[0].vendor, "NVIDIA"); + } + + #[test] + fn test_parse_lspci_gpu_output() { + let output = r#"01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA102 [GeForce RTX 3090] [10de:2204] (rev a1) +00:02.0 VGA compatible controller [0300]: Intel Corporation Device [8086:9a49] (rev 01)"#; + + let devices = parse_lspci_gpu_output(output).unwrap(); + + assert_eq!(devices.len(), 2); + assert_eq!(devices[0].vendor, "NVIDIA"); + assert_eq!(devices[1].vendor, "Intel"); + } + + #[test] + fn test_extract_pci_id() { + assert_eq!(extract_pci_id("[10de:2204]"), Some("10de:2204".to_string())); + assert_eq!(extract_pci_id("NVIDIA [10de:2204] (rev a1)"), Some("10de:2204".to_string())); + assert_eq!(extract_pci_id("No PCI ID here"), None); + } +} diff --git a/src/domain/parsers/mod.rs b/src/domain/parsers/mod.rs index 3fb592a..2ddb702 100644 --- a/src/domain/parsers/mod.rs +++ b/src/domain/parsers/mod.rs @@ -21,6 +21,7 @@ limitations under the License. pub mod common; pub mod cpu; +pub mod gpu; pub mod memory; pub mod network; pub mod storage; @@ -28,6 +29,7 @@ pub mod system; pub use common::*; pub use cpu::*; +pub use gpu::*; pub use memory::*; pub use network::*; pub use storage::*; diff --git a/src/domain/parsers/network.rs b/src/domain/parsers/network.rs index e500fbb..8a8b48b 100644 --- a/src/domain/parsers/network.rs +++ b/src/domain/parsers/network.rs @@ -36,6 +36,7 @@ pub fn parse_ip_output(ip_output: &str) -> Result, String> model: "Unknown".to_string(), pci_id: "Unknown".to_string(), numa_node: None, + ..Default::default() }); } } @@ -91,6 +92,7 @@ pub fn parse_macos_network_info(ifconfig_output: &str) -> Result Result { + let sectors: u64 = content + .trim() + .parse() + .map_err(|e| format!("Failed to parse sectors: {}", e))?; + Ok(sectors * 512) +} + +/// Parse sysfs rotational flag +/// +/// # Arguments +/// +/// * `content` - Content of `/sys/block/{dev}/queue/rotational` +/// +/// # Returns +/// +/// `true` if device is rotational (HDD), `false` if SSD/NVMe. +pub fn parse_sysfs_rotational(content: &str) -> bool { + content.trim() == "1" +} + +/// Check if device name indicates a virtual device +/// +/// Virtual devices should be filtered from physical storage lists. +/// +/// # Arguments +/// +/// * `name` - Device name (e.g., "sda", "loop0", "dm-0") +pub fn is_virtual_device(name: &str) -> bool { + name.starts_with("loop") + || name.starts_with("ram") + || name.starts_with("dm-") + || name.starts_with("sr") + || name.starts_with("fd") + || name.starts_with("zram") + || name.starts_with("nbd") +} + +/// Parse lsblk JSON output +/// +/// # Arguments +/// +/// * `output` - JSON output from `lsblk -J -b -d -o NAME,SIZE,TYPE,MODEL,SERIAL,ROTA,TRAN,WWN` +pub fn parse_lsblk_json(output: &str) -> Result, String> { + let json: serde_json::Value = serde_json::from_str(output) + .map_err(|e| format!("Failed to parse lsblk JSON: {}", e))?; + + let blockdevices = json + .get("blockdevices") + .and_then(|v| v.as_array()) + .ok_or_else(|| "Missing blockdevices array in lsblk output".to_string())?; + + let mut devices = Vec::new(); + + for device in blockdevices { + let name = device + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + // Skip virtual devices + if is_virtual_device(&name) { + continue; + } + + let size_bytes = device + .get("size") + .and_then(|v| v.as_u64()) + .unwrap_or(0); + + // Skip small devices + if size_bytes < 1_000_000_000 { + continue; + } + + let is_rotational = device + .get("rota") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let device_type = StorageType::from_device(&name, is_rotational); + + let model = device + .get("model") + .and_then(|v| v.as_str()) + .unwrap_or("") + .trim() + .to_string(); + + let serial_number = device + .get("serial") + .and_then(|v| v.as_str()) + .filter(|s| !s.is_empty()) + .map(|s| s.trim().to_string()); + + let wwn = device + .get("wwn") + .and_then(|v| v.as_str()) + .filter(|s| !s.is_empty()) + .map(|s| s.trim().to_string()); + + let interface = device + .get("tran") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_uppercase(); + + let mut storage_device = StorageDevice { + name: name.clone(), + device_path: format!("/dev/{}", name), + device_type: device_type.clone(), + type_: device_type.display_name().to_string(), + size_bytes, + model, + serial_number, + wwn, + interface, + is_rotational, + detection_method: "lsblk".to_string(), + ..Default::default() + }; + + storage_device.calculate_size_fields(); + devices.push(storage_device); + } + + Ok(devices) +} /// Parse storage devices from lsblk output pub fn parse_lsblk_output(lsblk_output: &str) -> Result, String> { @@ -36,6 +178,7 @@ pub fn parse_lsblk_output(lsblk_output: &str) -> Result, Stri type_: type_.to_string(), size: clean_value(&size), model: name.clone(), + ..Default::default() }); } } @@ -77,6 +220,7 @@ pub fn parse_macos_storage_info( type_: "ssd".to_string(), size: "Unknown".to_string(), model: format!("{model} (Apple Fabric)"), + ..Default::default() }); } else if trimmed.starts_with("Size:") && current_device.is_some() { // Extract size information @@ -99,6 +243,7 @@ pub fn parse_macos_storage_info( type_: "ssd".to_string(), size: "2 TB (1,995,218,165,760 bytes)".to_string(), model: "APPLE SSD AP2048Z (Apple Fabric)".to_string(), + ..Default::default() }); } From ce4a7aca2ddfb675ea2b6a0d1c3ac9e6562cfec6 Mon Sep 17 00:00:00 2001 From: "Kenny (Knight) Sheridan" Date: Mon, 5 Jan 2026 16:42:35 -0800 Subject: [PATCH 6/8] Remove docs directory --- docs/CPU_ENHANCEMENTS.md | 936 ------- docs/ENHANCEMENTS.md | 541 ---- docs/GPU_DETECTION.md | 955 ------- docs/IMPLEMENTATION_GUIDE.md | 3679 -------------------------- docs/LINUX_ADAPTER_IMPLEMENTATION.md | 1822 ------------- docs/MEMORY_ENHANCEMENTS.md | 662 ----- docs/NETWORK_ENHANCEMENTS.md | 620 ----- docs/RUSTDOC_STANDARDS.md | 560 ---- docs/STORAGE_DETECTION.md | 1136 -------- docs/TESTING_STRATEGY.md | 709 ----- 10 files changed, 11620 deletions(-) delete mode 100644 docs/CPU_ENHANCEMENTS.md delete mode 100644 docs/ENHANCEMENTS.md delete mode 100644 docs/GPU_DETECTION.md delete mode 100644 docs/IMPLEMENTATION_GUIDE.md delete mode 100644 docs/LINUX_ADAPTER_IMPLEMENTATION.md delete mode 100644 docs/MEMORY_ENHANCEMENTS.md delete mode 100644 docs/NETWORK_ENHANCEMENTS.md delete mode 100644 docs/RUSTDOC_STANDARDS.md delete mode 100644 docs/STORAGE_DETECTION.md delete mode 100644 docs/TESTING_STRATEGY.md diff --git a/docs/CPU_ENHANCEMENTS.md b/docs/CPU_ENHANCEMENTS.md deleted file mode 100644 index f95269e..0000000 --- a/docs/CPU_ENHANCEMENTS.md +++ /dev/null @@ -1,936 +0,0 @@ -# CPU Enhancement Plan - -> **Category:** Critical Issue -> **Target Platforms:** Linux (x86_64, aarch64) -> **Priority:** Critical - CPU frequency not exposed, cache sizes missing - -## Table of Contents - -1. [Problem Statement](#problem-statement) -2. [Current Implementation](#current-implementation) -3. [Multi-Method Detection Strategy](#multi-method-detection-strategy) -4. [Entity Changes](#entity-changes) -5. [Detection Method Details](#detection-method-details) -6. [Adapter Implementation](#adapter-implementation) -7. [Parser Implementation](#parser-implementation) -8. [Architecture-Specific Handling](#architecture-specific-handling) -9. [Testing Requirements](#testing-requirements) -10. [References](#references) - ---- - -## Problem Statement - -### Current Issue - -The `CpuInfo` structure lacks critical fields for CMDB inventory: - -```rust -// Current struct - limited fields -pub struct CpuInfo { - pub model: String, - pub cores: u32, - pub threads: u32, - pub sockets: u32, - pub speed: String, // String format, unreliable -} -``` - -Issues: -1. **Frequency as String** - `speed: "2300.000 MHz"` cannot be parsed reliably -2. **No cache information** - L1/L2/L3 cache sizes missing -3. **No architecture field** - Cannot distinguish x86_64 vs aarch64 -4. **No CPU flags** - Missing feature detection (AVX, SVE, etc.) - -### Impact - -- CMDB uses hardcoded 2100 MHz for CPU frequency -- Cannot assess cache hierarchy for performance analysis -- Cannot verify CPU features for workload compatibility - -### Requirements - -1. **Numeric frequency field** - `frequency_mhz: u32` -2. **Cache size fields** - L1d, L1i, L2, L3 in kilobytes -3. **Architecture detection** - x86_64, aarch64, etc. -4. **CPU flags/features** - Vector extensions, virtualization, etc. -5. **Multi-method detection** - sysfs, CPUID, lscpu, sysinfo - ---- - -## Current Implementation - -### Location - -- **Entity:** `src/domain/entities.rs:163-175` -- **Adapter:** `src/adapters/secondary/system/linux.rs:67-102` -- **Parser:** `src/domain/parsers/cpu.rs` - -### Current Detection Flow - -``` -┌─────────────────────────────────────────┐ -│ LinuxSystemInfoProvider::get_cpu_info() │ -└─────────────────────────────────────────┘ - │ - ▼ - ┌──────────────────┐ - │ lscpu │ - └──────────────────┘ - │ - ▼ - ┌──────────────────────┐ - │ dmidecode -t processor│ - │ (requires privileges) │ - └──────────────────────┘ - │ - ▼ - Combine and return CpuInfo -``` - -### Current Limitations - -| Limitation | Impact | -|------------|--------| -| No sysfs reads | Misses cpufreq data | -| No CPUID access | Misses cache details on x86 | -| Speed as string | Consumer parsing issues | -| No cache info | Missing CMDB fields | -| No flags/features | Cannot verify capabilities | - ---- - -## Multi-Method Detection Strategy - -### Detection Priority Chain - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ CPU DETECTION CHAIN │ -│ │ -│ Priority 1: sysfs /sys/devices/system/cpu │ -│ ├── Most reliable for frequency and cache │ -│ ├── Works on all Linux architectures │ -│ ├── cpufreq for frequency │ -│ └── cache/index* for cache sizes │ -│ │ │ -│ ▼ (for x86 detailed cache info) │ -│ Priority 2: raw-cpuid crate (x86/x86_64 only) │ -│ ├── Direct CPUID instruction access │ -│ ├── Accurate cache line/associativity info │ -│ ├── CPU features and flags │ -│ └── Feature-gated: #[cfg(feature = "x86-cpu")] │ -│ │ │ -│ ▼ (for model and topology) │ -│ Priority 3: /proc/cpuinfo │ -│ ├── Model name │ -│ ├── Vendor │ -│ ├── Flags (x86) │ -│ └── Features (ARM) │ -│ │ │ -│ ▼ (for topology) │ -│ Priority 4: lscpu command │ -│ ├── Socket/core/thread topology │ -│ ├── NUMA information │ -│ └── Architecture detection │ -│ │ │ -│ ▼ (for SMBIOS data) │ -│ Priority 5: dmidecode -t processor │ -│ ├── Serial number (on some systems) │ -│ ├── Max frequency │ -│ └── Requires privileges │ -│ │ │ -│ ▼ (cross-platform fallback) │ -│ Priority 6: sysinfo crate │ -│ ├── Basic CPU info │ -│ ├── Cross-platform │ -│ └── Limited detail │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -### Method Capabilities Matrix - -| Method | Frequency | Cache | Model | Vendor | Flags | Topology | Arch | -|--------|-----------|-------|-------|--------|-------|----------|------| -| sysfs | Yes | Yes | No | No | No | Partial | Yes | -| raw-cpuid | Yes | Yes | Yes | Yes | Yes | No | x86 only | -| /proc/cpuinfo | No | No | Yes | Yes | Yes | Partial | Yes | -| lscpu | Partial | Partial | Yes | Yes | Partial | Yes | Yes | -| dmidecode | Yes | No | Yes | Yes | No | Partial | Yes | -| sysinfo | Yes | No | Partial | No | No | Yes | Yes | - ---- - -## Entity Changes - -### New CpuInfo Structure - -```rust -// src/domain/entities.rs - -/// CPU cache level information -/// -/// Represents a single cache level (L1d, L1i, L2, L3). -/// -/// # References -/// -/// - [Intel CPUID](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) -/// - [Linux cache sysfs](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-system-cpu) -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct CpuCacheInfo { - /// Cache level (1, 2, or 3) - pub level: u8, - - /// Cache type: "Data", "Instruction", or "Unified" - pub cache_type: String, - - /// Cache size in kilobytes - pub size_kb: u32, - - /// Number of ways of associativity - pub ways_of_associativity: Option, - - /// Cache line size in bytes - pub line_size_bytes: Option, - - /// Number of sets - pub sets: Option, - - /// Whether this cache is shared across cores - pub shared_cpu_map: Option, -} - -/// CPU information with extended details -/// -/// Provides comprehensive CPU information for CMDB inventory, -/// including frequency, cache hierarchy, and feature flags. -/// -/// # Detection Methods -/// -/// CPU information is gathered from multiple sources in priority order: -/// 1. **sysfs** - `/sys/devices/system/cpu` (frequency, cache) -/// 2. **raw-cpuid** - CPUID instruction (x86 only, cache details) -/// 3. **/proc/cpuinfo** - Model, vendor, flags -/// 4. **lscpu** - Topology information -/// 5. **dmidecode** - SMBIOS data (requires privileges) -/// 6. **sysinfo** - Cross-platform fallback -/// -/// # Frequency Values -/// -/// Multiple frequency values are provided: -/// - `frequency_mhz` - Current or maximum frequency (primary field) -/// - `frequency_min_mhz` - Minimum scaling frequency -/// - `frequency_max_mhz` - Maximum scaling frequency -/// - `frequency_base_mhz` - Base (non-turbo) frequency -/// -/// # Cache Hierarchy -/// -/// Cache sizes are provided per-core in kilobytes: -/// - `cache_l1d_kb` - L1 data cache -/// - `cache_l1i_kb` - L1 instruction cache -/// - `cache_l2_kb` - L2 cache (may be per-core or shared) -/// - `cache_l3_kb` - L3 cache (typically shared) -/// -/// # Example -/// -/// ``` -/// use hardware_report::CpuInfo; -/// -/// // Calculate total L3 cache -/// let total_l3_mb = cpu.cache_l3_kb.unwrap_or(0) as f64 / 1024.0; -/// -/// // Check for AVX-512 support -/// let has_avx512 = cpu.flags.iter().any(|f| f.starts_with("avx512")); -/// ``` -/// -/// # References -/// -/// - [Linux CPU sysfs](https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.rst) -/// - [Intel CPUID](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) -/// - [ARM CPU ID registers](https://developer.arm.com/documentation/ddi0487/latest) -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct CpuInfo { - /// CPU model name - /// - /// Examples: - /// - "AMD EPYC 7763 64-Core Processor" - /// - "Intel(R) Xeon(R) Platinum 8380 CPU @ 2.30GHz" - /// - "Neoverse-N1" - pub model: String, - - /// CPU vendor identifier - /// - /// Values: - /// - "GenuineIntel" (Intel) - /// - "AuthenticAMD" (AMD) - /// - "ARM" (ARM/Ampere/etc) - pub vendor: String, - - /// Number of physical cores per socket - pub cores: u32, - - /// Number of threads per core (hyperthreading/SMT) - pub threads: u32, - - /// Number of CPU sockets - pub sockets: u32, - - /// Total physical cores (cores * sockets) - pub total_cores: u32, - - /// Total logical CPUs (cores * threads * sockets) - pub total_threads: u32, - - /// CPU frequency in MHz (current or max) - /// - /// Primary frequency field. This is the most useful value for - /// general CMDB purposes. - pub frequency_mhz: u32, - - /// Minimum scaling frequency in MHz - /// - /// From cpufreq scaling_min_freq. - pub frequency_min_mhz: Option, - - /// Maximum scaling frequency in MHz - /// - /// From cpufreq scaling_max_freq. This is the turbo/boost frequency. - pub frequency_max_mhz: Option, - - /// Base (non-turbo) frequency in MHz - /// - /// From cpufreq base_frequency or CPUID. - pub frequency_base_mhz: Option, - - /// CPU architecture - /// - /// Values: "x86_64", "aarch64", "armv7l", etc. - pub architecture: String, - - /// CPU microarchitecture name - /// - /// Examples: - /// - "Zen3" (AMD) - /// - "Ice Lake" (Intel) - /// - "Neoverse N1" (ARM) - pub microarchitecture: Option, - - /// L1 data cache size in kilobytes (per core) - pub cache_l1d_kb: Option, - - /// L1 instruction cache size in kilobytes (per core) - pub cache_l1i_kb: Option, - - /// L2 cache size in kilobytes (per core typically) - pub cache_l2_kb: Option, - - /// L3 cache size in kilobytes (typically shared) - /// - /// Note: This is often the total L3 across all cores in a socket. - pub cache_l3_kb: Option, - - /// Detailed cache information for each level - pub caches: Vec, - - /// CPU flags/features - /// - /// Examples (x86): "avx", "avx2", "avx512f", "aes", "sse4_2" - /// Examples (ARM): "fp", "asimd", "sve", "sve2" - /// - /// # References - /// - /// - [x86 CPUID flags](https://en.wikipedia.org/wiki/CPUID) - /// - [ARM HWCAP](https://www.kernel.org/doc/html/latest/arm64/elf_hwcaps.html) - pub flags: Vec, - - /// Microcode/firmware version - pub microcode_version: Option, - - /// CPU stepping (revision level) - pub stepping: Option, - - /// CPU family number - pub family: Option, - - /// CPU model number (not the name) - pub model_number: Option, - - /// Virtualization support - /// - /// Values: "VT-x", "AMD-V", "none", etc. - pub virtualization: Option, - - /// NUMA nodes count - pub numa_nodes: u32, - - /// Detection methods used - pub detection_methods: Vec, -} - -impl Default for CpuInfo { - fn default() -> Self { - Self { - model: String::new(), - vendor: String::new(), - cores: 0, - threads: 1, - sockets: 1, - total_cores: 0, - total_threads: 0, - frequency_mhz: 0, - frequency_min_mhz: None, - frequency_max_mhz: None, - frequency_base_mhz: None, - architecture: std::env::consts::ARCH.to_string(), - microarchitecture: None, - cache_l1d_kb: None, - cache_l1i_kb: None, - cache_l2_kb: None, - cache_l3_kb: None, - caches: Vec::new(), - flags: Vec::new(), - microcode_version: None, - stepping: None, - family: None, - model_number: None, - virtualization: None, - numa_nodes: 1, - detection_methods: Vec::new(), - } - } -} -``` - ---- - -## Detection Method Details - -### Method 1: sysfs /sys/devices/system/cpu - -**When:** Linux systems (always primary for freq/cache) - -**sysfs paths:** - -``` -/sys/devices/system/cpu/ -├── cpu0/ -│ ├── cpufreq/ -│ │ ├── cpuinfo_max_freq # Max frequency in kHz -│ │ ├── cpuinfo_min_freq # Min frequency in kHz -│ │ ├── scaling_cur_freq # Current frequency in kHz -│ │ ├── scaling_max_freq # Scaling max in kHz -│ │ ├── scaling_min_freq # Scaling min in kHz -│ │ └── base_frequency # Base (non-turbo) freq -│ ├── cache/ -│ │ ├── index0/ # L1d typically -│ │ │ ├── level # Cache level (1, 2, 3) -│ │ │ ├── type # Data, Instruction, Unified -│ │ │ ├── size # Size with unit (e.g., "32K") -│ │ │ ├── ways_of_associativity -│ │ │ ├── coherency_line_size -│ │ │ └── number_of_sets -│ │ ├── index1/ # L1i typically -│ │ ├── index2/ # L2 typically -│ │ └── index3/ # L3 typically -│ └── topology/ -│ ├── physical_package_id # Socket ID -│ ├── core_id # Core ID within socket -│ └── thread_siblings_list # SMT siblings -├── possible # Possible CPU range -├── present # Present CPU range -└── online # Online CPU range -``` - -**Frequency parsing:** - -```rust -// sysfs reports frequency in kHz, convert to MHz -let freq_khz: u32 = read_sysfs("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq")? - .trim() - .parse()?; -let freq_mhz = freq_khz / 1000; -``` - -**Cache size parsing:** - -```rust -// sysfs reports size as "32K", "512K", "32768K", "16M", etc. -fn parse_cache_size(size_str: &str) -> Option { - let s = size_str.trim(); - if s.ends_with('K') { - s[..s.len()-1].parse().ok() - } else if s.ends_with('M') { - s[..s.len()-1].parse::().ok().map(|v| v * 1024) - } else { - s.parse().ok() - } -} -``` - -**References:** -- [CPU sysfs Documentation](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-system-cpu) -- [cpufreq User Guide](https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.rst) - ---- - -### Method 2: raw-cpuid (x86/x86_64 only) - -**When:** x86/x86_64 architecture, feature enabled - -**Cargo.toml:** -```toml -[target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies] -raw-cpuid = { version = "11", optional = true } - -[features] -x86-cpu = ["raw-cpuid"] -``` - -**Usage:** -```rust -#[cfg(all(feature = "x86-cpu", any(target_arch = "x86", target_arch = "x86_64")))] -fn get_cpu_info_cpuid() -> CpuInfo { - use raw_cpuid::CpuId; - - let cpuid = CpuId::new(); - - let model = cpuid.get_processor_brand_string() - .map(|b| b.as_str().trim().to_string()) - .unwrap_or_default(); - - let vendor = cpuid.get_vendor_info() - .map(|v| v.as_str().to_string()) - .unwrap_or_default(); - - // Cache info - if let Some(cache_params) = cpuid.get_cache_parameters() { - for cache in cache_params { - let size_kb = (cache.associativity() - * cache.physical_line_partitions() - * cache.coherency_line_size() - * cache.sets()) as u32 / 1024; - // ... - } - } - - // Feature flags - if let Some(features) = cpuid.get_feature_info() { - // Check SSE, AVX, etc. - } - - // ... -} -``` - -**References:** -- [raw-cpuid crate](https://docs.rs/raw-cpuid) -- [Intel CPUID Reference](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) - ---- - -### Method 3: /proc/cpuinfo - -**When:** Linux, for model name and flags - -**Path:** `/proc/cpuinfo` - -**Format (x86):** -``` -processor : 0 -vendor_id : GenuineIntel -cpu family : 6 -model : 106 -model name : Intel(R) Xeon(R) Platinum 8380 CPU @ 2.30GHz -stepping : 6 -microcode : 0xd0003a5 -cpu MHz : 2300.000 -cache size : 61440 KB -flags : fpu vme de pse ... avx avx2 avx512f avx512dq ... -``` - -**Format (ARM):** -``` -processor : 0 -BogoMIPS : 50.00 -Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 ... -CPU implementer : 0x41 -CPU architecture: 8 -CPU variant : 0x3 -CPU part : 0xd0c -CPU revision : 1 -``` - -**References:** -- [/proc/cpuinfo](https://man7.org/linux/man-pages/man5/proc.5.html) - ---- - -### Method 4: lscpu - -**When:** For topology information - -**Command:** -```bash -lscpu -J # JSON output (preferred) -lscpu # Text output (fallback) -``` - -**JSON output:** -```json -{ - "lscpu": [ - {"field": "Architecture:", "data": "x86_64"}, - {"field": "CPU(s):", "data": "128"}, - {"field": "Thread(s) per core:", "data": "2"}, - {"field": "Core(s) per socket:", "data": "32"}, - {"field": "Socket(s):", "data": "2"}, - {"field": "NUMA node(s):", "data": "2"}, - {"field": "Model name:", "data": "AMD EPYC 7763 64-Core Processor"}, - {"field": "CPU max MHz:", "data": "3500.0000"}, - {"field": "L1d cache:", "data": "2 MiB"}, - {"field": "L1i cache:", "data": "2 MiB"}, - {"field": "L2 cache:", "data": "32 MiB"}, - {"field": "L3 cache:", "data": "256 MiB"} - ] -} -``` - -**References:** -- [lscpu man page](https://man7.org/linux/man-pages/man1/lscpu.1.html) - ---- - -### Method 5: sysinfo crate - -**When:** Cross-platform fallback - -**Usage:** -```rust -use sysinfo::System; - -let sys = System::new_all(); - -let frequency = sys.cpus().first() - .map(|cpu| cpu.frequency() as u32) - .unwrap_or(0); - -let physical_cores = sys.physical_core_count().unwrap_or(0); -let logical_cpus = sys.cpus().len(); -``` - -**References:** -- [sysinfo crate](https://docs.rs/sysinfo) - ---- - -## Architecture-Specific Handling - -### x86_64 - -```rust -#[cfg(target_arch = "x86_64")] -fn detect_cpu_x86(info: &mut CpuInfo) { - // Use raw-cpuid if available - #[cfg(feature = "x86-cpu")] - { - use raw_cpuid::CpuId; - let cpuid = CpuId::new(); - // Extract cache, features, etc. - } - - // Read flags from /proc/cpuinfo - // Parse "flags" line -} -``` - -### aarch64 - -```rust -#[cfg(target_arch = "aarch64")] -fn detect_cpu_arm(info: &mut CpuInfo) { - // Read from /proc/cpuinfo - // "Features" line instead of "flags" - // "CPU part" for microarchitecture detection - - // Map CPU part to microarchitecture - // 0xd0c -> "Neoverse N1" - // 0xd40 -> "Neoverse V1" - // etc. -} - -/// Map ARM CPU part ID to microarchitecture name -/// -/// # References -/// -/// - [ARM CPU Part Numbers](https://developer.arm.com/documentation/ddi0487/latest) -fn arm_cpu_part_to_name(part: &str) -> Option<&'static str> { - match part.to_lowercase().as_str() { - "0xd03" => Some("Cortex-A53"), - "0xd07" => Some("Cortex-A57"), - "0xd08" => Some("Cortex-A72"), - "0xd09" => Some("Cortex-A73"), - "0xd0a" => Some("Cortex-A75"), - "0xd0b" => Some("Cortex-A76"), - "0xd0c" => Some("Neoverse N1"), - "0xd0d" => Some("Cortex-A77"), - "0xd40" => Some("Neoverse V1"), - "0xd41" => Some("Cortex-A78"), - "0xd44" => Some("Cortex-X1"), - "0xd49" => Some("Neoverse N2"), - "0xd4f" => Some("Neoverse V2"), - _ => None, - } -} -``` - ---- - -## Parser Implementation - -### File: `src/domain/parsers/cpu.rs` - -```rust -//! CPU information parsing functions -//! -//! This module provides pure parsing functions for CPU information from -//! various sources. All functions take string input and return parsed -//! results without performing I/O. -//! -//! # Supported Formats -//! -//! - sysfs frequency/cache files -//! - /proc/cpuinfo -//! - lscpu text and JSON output -//! - dmidecode processor output -//! -//! # References -//! -//! - [Linux CPU sysfs](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-system-cpu) -//! - [/proc/cpuinfo](https://man7.org/linux/man-pages/man5/proc.5.html) - -use crate::domain::{CpuCacheInfo, CpuInfo}; - -/// Parse sysfs frequency file (in kHz) to MHz -/// -/// # Arguments -/// -/// * `content` - Content of cpufreq file (e.g., scaling_max_freq) -/// -/// # Returns -/// -/// Frequency in MHz. -/// -/// # Example -/// -/// ``` -/// use hardware_report::domain::parsers::cpu::parse_sysfs_freq_khz; -/// -/// assert_eq!(parse_sysfs_freq_khz("3500000").unwrap(), 3500); -/// ``` -pub fn parse_sysfs_freq_khz(content: &str) -> Result { - let khz: u32 = content.trim().parse() - .map_err(|e| format!("Invalid frequency: {}", e))?; - Ok(khz / 1000) -} - -/// Parse sysfs cache size (e.g., "32K", "1M") -/// -/// # Arguments -/// -/// * `content` - Content of cache size file -/// -/// # Returns -/// -/// Size in kilobytes. -/// -/// # Example -/// -/// ``` -/// use hardware_report::domain::parsers::cpu::parse_sysfs_cache_size; -/// -/// assert_eq!(parse_sysfs_cache_size("32K").unwrap(), 32); -/// assert_eq!(parse_sysfs_cache_size("1M").unwrap(), 1024); -/// assert_eq!(parse_sysfs_cache_size("256M").unwrap(), 262144); -/// ``` -pub fn parse_sysfs_cache_size(content: &str) -> Result { - let s = content.trim(); - if s.ends_with('K') { - s[..s.len()-1].parse() - .map_err(|e| format!("Invalid cache size: {}", e)) - } else if s.ends_with('M') { - s[..s.len()-1].parse::() - .map(|v| v * 1024) - .map_err(|e| format!("Invalid cache size: {}", e)) - } else if s.ends_with('G') { - s[..s.len()-1].parse::() - .map(|v| v * 1024 * 1024) - .map_err(|e| format!("Invalid cache size: {}", e)) - } else { - s.parse() - .map_err(|e| format!("Invalid cache size: {}", e)) - } -} - -/// Parse /proc/cpuinfo content -/// -/// # Arguments -/// -/// * `content` - Full content of /proc/cpuinfo -/// -/// # Returns -/// -/// Partial CpuInfo with fields from cpuinfo. -/// -/// # References -/// -/// - [/proc filesystem](https://man7.org/linux/man-pages/man5/proc.5.html) -pub fn parse_proc_cpuinfo(content: &str) -> Result { - let mut info = CpuInfo::default(); - - for line in content.lines() { - let parts: Vec<&str> = line.splitn(2, ':').collect(); - if parts.len() != 2 { - continue; - } - - let key = parts[0].trim(); - let value = parts[1].trim(); - - match key { - "model name" => info.model = value.to_string(), - "vendor_id" => info.vendor = value.to_string(), - "cpu family" => info.family = value.parse().ok(), - "model" => info.model_number = value.parse().ok(), - "stepping" => info.stepping = value.parse().ok(), - "microcode" => info.microcode_version = Some(value.to_string()), - "flags" | "Features" => { - info.flags = value.split_whitespace() - .map(String::from) - .collect(); - } - "CPU implementer" => { - if info.vendor.is_empty() { - info.vendor = "ARM".to_string(); - } - } - "CPU part" => { - // ARM CPU part number - if let Some(arch) = arm_cpu_part_to_name(value) { - info.microarchitecture = Some(arch.to_string()); - } - } - _ => {} - } - } - - Ok(info) -} - -/// Parse lscpu JSON output -/// -/// # Arguments -/// -/// * `output` - JSON output from `lscpu -J` -/// -/// # References -/// -/// - [lscpu](https://man7.org/linux/man-pages/man1/lscpu.1.html) -pub fn parse_lscpu_json(output: &str) -> Result { - todo!() -} - -/// Parse lscpu text output -/// -/// # Arguments -/// -/// * `output` - Text output from `lscpu` -pub fn parse_lscpu_text(output: &str) -> Result { - todo!() -} - -/// Map ARM CPU part ID to microarchitecture name -/// -/// # Arguments -/// -/// * `part` - CPU part from /proc/cpuinfo (e.g., "0xd0c") -/// -/// # References -/// -/// - [ARM CPU Part Numbers](https://developer.arm.com/documentation/ddi0487/latest) -pub fn arm_cpu_part_to_name(part: &str) -> Option<&'static str> { - match part.to_lowercase().as_str() { - "0xd03" => Some("Cortex-A53"), - "0xd07" => Some("Cortex-A57"), - "0xd08" => Some("Cortex-A72"), - "0xd09" => Some("Cortex-A73"), - "0xd0a" => Some("Cortex-A75"), - "0xd0b" => Some("Cortex-A76"), - "0xd0c" => Some("Neoverse N1"), - "0xd0d" => Some("Cortex-A77"), - "0xd40" => Some("Neoverse V1"), - "0xd41" => Some("Cortex-A78"), - "0xd44" => Some("Cortex-X1"), - "0xd49" => Some("Neoverse N2"), - "0xd4f" => Some("Neoverse V2"), - "0xd80" => Some("Cortex-A520"), - "0xd81" => Some("Cortex-A720"), - _ => None, - } -} -``` - ---- - -## Testing Requirements - -### Unit Tests - -| Test | Description | -|------|-------------| -| `test_parse_sysfs_freq` | Parse frequency in kHz to MHz | -| `test_parse_cache_size` | Parse K/M/G suffixes | -| `test_parse_proc_cpuinfo_intel` | Parse Intel cpuinfo | -| `test_parse_proc_cpuinfo_amd` | Parse AMD cpuinfo | -| `test_parse_proc_cpuinfo_arm` | Parse ARM cpuinfo | -| `test_parse_lscpu_json` | Parse lscpu JSON | -| `test_arm_cpu_part_mapping` | ARM part to name | - -### Integration Tests - -| Test | Platform | Description | -|------|----------|-------------| -| `test_cpu_detection_x86` | x86_64 | Full detection on x86 | -| `test_cpu_detection_arm` | aarch64 | Full detection on ARM | -| `test_cpuid_features` | x86_64 | raw-cpuid feature detection | -| `test_sysfs_cache` | Linux | sysfs cache reading | - ---- - -## References - -### Official Documentation - -| Resource | URL | -|----------|-----| -| Linux CPU sysfs | https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-system-cpu | -| Linux cpufreq | https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.rst | -| Intel CPUID | https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html | -| AMD CPUID | https://www.amd.com/en/support/tech-docs | -| ARM CPU ID | https://developer.arm.com/documentation/ddi0487/latest | -| ARM HWCAP | https://www.kernel.org/doc/html/latest/arm64/elf_hwcaps.html | - -### Crate Documentation - -| Crate | URL | -|-------|-----| -| raw-cpuid | https://docs.rs/raw-cpuid | -| sysinfo | https://docs.rs/sysinfo | - ---- - -## Changelog - -| Date | Changes | -|------|---------| -| 2024-12-29 | Initial specification | diff --git a/docs/ENHANCEMENTS.md b/docs/ENHANCEMENTS.md deleted file mode 100644 index 052becd..0000000 --- a/docs/ENHANCEMENTS.md +++ /dev/null @@ -1,541 +0,0 @@ -# Hardware Report Enhancement Plan - -> **Version:** 0.2.0 -> **Target:** Linux (primary), macOS (secondary) -> **Architecture Focus:** x86_64, aarch64/ARM64 - -## Table of Contents - -1. [Overview](#overview) -2. [Architecture Principles](#architecture-principles) -3. [Enhancement Summary](#enhancement-summary) -4. [Phase 1: Critical Issues](#phase-1-critical-issues) -5. [Phase 2: Data Gaps](#phase-2-data-gaps) -6. [Phase 3: Runtime Metrics](#phase-3-runtime-metrics) -7. [New Dependencies](#new-dependencies) -8. [Implementation Order](#implementation-order) -9. [Related Documents](#related-documents) - ---- - -## Overview - -This document outlines the implementation plan for enhancing the `hardware_report` crate to better serve CMDB (Configuration Management Database) inventory use cases, specifically addressing issues encountered in the `metal-agent` project. - -### Goals - -1. **Eliminate fallback collection methods** - Provide complete, accurate data natively -2. **Multi-architecture support** - Full functionality on x86_64 and aarch64 (ARM64) -3. **Numeric data formats** - Return parseable numeric values, not formatted strings -4. **Multi-method detection** - Use multiple detection strategies with graceful fallbacks -5. **Comprehensive documentation** - Rustdoc for all public APIs with links to official references - -### Non-Goals - -- Windows support (out of scope for this phase) -- Real-time monitoring (basic runtime metrics only) -- Container/VM detection improvements - ---- - -## Architecture Principles - -This implementation strictly follows the **Hexagonal Architecture (Ports and Adapters)** pattern already established in the codebase. - -### Layer Responsibilities - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ DOMAIN LAYER │ -│ src/domain/ │ -│ ├── entities.rs # Data structures (platform-agnostic) │ -│ ├── errors.rs # Domain errors │ -│ ├── parsers/ # Pure parsing functions (no I/O) │ -│ │ ├── cpu.rs │ -│ │ ├── memory.rs │ -│ │ ├── storage.rs │ -│ │ ├── network.rs │ -│ │ └── gpu.rs # NEW │ -│ └── services/ # Domain services (orchestration) │ -└─────────────────────────────────────────────────────────────────────┘ - │ - │ implements - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ PORTS LAYER │ -│ src/ports/ │ -│ ├── primary/ # Offered interfaces (what we provide) │ -│ │ └── reporting.rs # HardwareReportingService trait │ -│ └── secondary/ # Required interfaces (what we need) │ -│ ├── system.rs # SystemInfoProvider trait │ -│ ├── command.rs # CommandExecutor trait │ -│ └── publisher.rs # DataPublisher trait │ -└─────────────────────────────────────────────────────────────────────┘ - │ - │ implemented by - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ ADAPTERS LAYER │ -│ src/adapters/secondary/ │ -│ ├── system/ │ -│ │ ├── linux.rs # LinuxSystemInfoProvider │ -│ │ └── macos.rs # MacOSSystemInfoProvider │ -│ ├── command/ │ -│ │ └── unix.rs # UnixCommandExecutor │ -│ └── publisher/ │ -│ ├── http.rs # HttpDataPublisher │ -│ └── file.rs # FileDataPublisher │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -### Key Principles - -| Principle | Description | -|-----------|-------------| -| **Domain Independence** | Domain layer has NO knowledge of adapters or I/O | -| **Pure Parsers** | Parsing functions take strings, return Results - no side effects | -| **Trait Abstraction** | All platform-specific code behind trait interfaces | -| **Multi-Method Detection** | Each adapter tries multiple methods, returns best result | -| **Graceful Degradation** | Partial data is better than no data - always return something | - ---- - -## Enhancement Summary - -### Critical Issues (Phase 1) - -| Issue | Impact | Solution | Doc | -|-------|--------|----------|-----| -| GPU memory returns unparseable string | CMDB shows 0MB VRAM | Numeric fields + multi-method detection | [GPU_DETECTION.md](./GPU_DETECTION.md) | -| Storage empty on ARM/aarch64 | No storage inventory | sysfs + sysinfo fallback chain | [STORAGE_DETECTION.md](./STORAGE_DETECTION.md) | -| CPU frequency not exposed | Hardcoded values | sysfs + raw-cpuid | [CPU_ENHANCEMENTS.md](./CPU_ENHANCEMENTS.md) | - -### Data Gaps (Phase 2) - -| Missing Field | Category | Priority | Doc | -|---------------|----------|----------|-----| -| CPU cache sizes (L1/L2/L3) | CPU | Medium | [CPU_ENHANCEMENTS.md](./CPU_ENHANCEMENTS.md) | -| DIMM part_number | Memory | Medium | [MEMORY_ENHANCEMENTS.md](./MEMORY_ENHANCEMENTS.md) | -| Storage serial_number | Storage | High | [STORAGE_DETECTION.md](./STORAGE_DETECTION.md) | -| Storage firmware_version | Storage | Medium | [STORAGE_DETECTION.md](./STORAGE_DETECTION.md) | -| GPU driver_version | GPU | High | [GPU_DETECTION.md](./GPU_DETECTION.md) | -| Network driver_version | Network | Low | [NETWORK_ENHANCEMENTS.md](./NETWORK_ENHANCEMENTS.md) | - ---- - -## Phase 1: Critical Issues - -### 1.1 GPU Detection Overhaul - -**Problem:** GPU memory returned as `"80 GB"` string, consumers can't parse numeric values. - -**Solution:** Multi-method detection with numeric output fields. - -See: [GPU_DETECTION.md](./GPU_DETECTION.md) - -#### Entity Changes - -```rust -// src/domain/entities.rs - -/// GPU device information -/// -/// Represents a discrete or integrated GPU detected in the system. -/// Memory values are provided in megabytes as unsigned integers for -/// reliable parsing by CMDB consumers. -/// -/// # Detection Methods -/// -/// GPUs are detected using multiple methods in priority order: -/// 1. NVML (NVIDIA Management Library) - most accurate for NVIDIA GPUs -/// 2. nvidia-smi command - fallback for NVIDIA when NVML unavailable -/// 3. ROCm SMI - AMD GPU detection -/// 4. sysfs /sys/class/drm - Linux DRM subsystem -/// 5. lspci - PCI device enumeration -/// 6. sysinfo crate - cross-platform fallback -/// -/// # References -/// -/// - [NVIDIA NVML Documentation](https://developer.nvidia.com/nvidia-management-library-nvml) -/// - [Linux DRM Subsystem](https://www.kernel.org/doc/html/latest/gpu/drm-uapi.html) -/// - [PCI ID Database](https://pci-ids.ucw.cz/) -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct GpuDevice { - /// GPU index (0-based) - pub index: u32, - - /// GPU product name (e.g., "NVIDIA H100 80GB HBM3") - pub name: String, - - /// GPU UUID (unique identifier) - pub uuid: String, - - /// Total GPU memory in megabytes - /// - /// This replaces the previous `memory: String` field which returned - /// formatted strings like "80 GB" that were difficult to parse. - pub memory_total_mb: u64, - - /// Available GPU memory in megabytes (runtime value, may be None if not queryable) - pub memory_free_mb: Option, - - /// GPU memory as formatted string (deprecated, for backward compatibility) - #[deprecated(since = "0.2.0", note = "Use memory_total_mb instead")] - #[serde(skip_serializing_if = "Option::is_none")] - pub memory: Option, - - /// PCI ID in format "vendor:device" (e.g., "10de:2330") - pub pci_id: String, - - /// PCI bus address (e.g., "0000:01:00.0") - pub pci_bus_id: Option, - - /// Vendor name (e.g., "NVIDIA", "AMD", "Intel") - pub vendor: String, - - /// Driver version (e.g., "535.129.03") - pub driver_version: Option, - - /// CUDA compute capability for NVIDIA GPUs (e.g., "9.0") - pub compute_capability: Option, - - /// GPU architecture (e.g., "Hopper", "Ada Lovelace", "RDNA3") - pub architecture: Option, - - /// NUMA node affinity (-1 if not applicable) - pub numa_node: Option, - - /// Detection method used to discover this GPU - pub detection_method: String, -} -``` - -### 1.2 Storage Detection on ARM - -**Problem:** `lsblk` returns empty on some ARM platforms. - -**Solution:** Multi-method detection with sysfs as primary on Linux. - -See: [STORAGE_DETECTION.md](./STORAGE_DETECTION.md) - -#### Entity Changes - -```rust -// src/domain/entities.rs - -/// Storage device type classification -/// -/// # References -/// -/// - [Linux Block Device Documentation](https://www.kernel.org/doc/html/latest/block/index.html) -/// - [NVMe Specification](https://nvmexpress.org/specifications/) -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -pub enum StorageType { - /// NVMe solid-state drive - Nvme, - /// SATA/SAS solid-state drive - Ssd, - /// Hard disk drive (rotational) - Hdd, - /// Embedded MMC storage - Emmc, - /// Unknown or unclassified storage type - Unknown, -} - -/// Storage device information -/// -/// # Detection Methods -/// -/// Storage devices are detected using multiple methods in priority order: -/// 1. sysfs /sys/block - direct kernel interface (Linux) -/// 2. lsblk command - block device listing -/// 3. sysinfo crate - cross-platform fallback -/// 4. diskutil (macOS) -/// -/// # References -/// -/// - [Linux sysfs Block Devices](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-block) -/// - [SMART Attributes](https://en.wikipedia.org/wiki/S.M.A.R.T.) -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct StorageDevice { - /// Device name (e.g., "nvme0n1", "sda") - pub name: String, - - /// Device type classification - pub device_type: StorageType, - - /// Legacy type field (deprecated) - #[deprecated(since = "0.2.0", note = "Use device_type instead")] - #[serde(skip_serializing_if = "Option::is_none")] - pub type_: Option, - - /// Device size in bytes - pub size_bytes: u64, - - /// Device size in gigabytes (convenience field) - pub size_gb: f64, - - /// Legacy size field as string (deprecated) - #[deprecated(since = "0.2.0", note = "Use size_bytes or size_gb instead")] - #[serde(skip_serializing_if = "Option::is_none")] - pub size: Option, - - /// Device model name - pub model: String, - - /// Device serial number (may require elevated privileges) - pub serial_number: Option, - - /// Device firmware version - pub firmware_version: Option, - - /// Interface type (e.g., "NVMe", "SATA", "SAS", "eMMC") - pub interface: String, - - /// Whether the device is rotational (true = HDD, false = SSD/NVMe) - pub is_rotational: bool, - - /// WWN (World Wide Name) if available - pub wwn: Option, - - /// Detection method used - pub detection_method: String, -} -``` - -### 1.3 CPU Frequency and Cache - -**Problem:** CPU frequency hardcoded, cache sizes not exposed. - -**Solution:** sysfs reads + raw-cpuid for x86. - -See: [CPU_ENHANCEMENTS.md](./CPU_ENHANCEMENTS.md) - -#### Entity Changes - -```rust -// src/domain/entities.rs - -/// CPU information with extended details -/// -/// # Detection Methods -/// -/// CPU information is gathered from multiple sources: -/// 1. sysfs /sys/devices/system/cpu - frequency and cache (Linux) -/// 2. /proc/cpuinfo - model and features (Linux) -/// 3. raw-cpuid crate - x86 CPUID instruction -/// 4. lscpu command - topology information -/// 5. dmidecode - SMBIOS data (requires privileges) -/// 6. sysinfo crate - cross-platform fallback -/// -/// # References -/// -/// - [Linux CPU sysfs Interface](https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.rst) -/// - [Intel CPUID Reference](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) -/// - [ARM CPU Identification](https://developer.arm.com/documentation/ddi0487/latest) -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct CpuInfo { - /// CPU model name (e.g., "AMD EPYC 7763 64-Core Processor") - pub model: String, - - /// CPU vendor (e.g., "GenuineIntel", "AuthenticAMD", "ARM") - pub vendor: String, - - /// Number of physical cores per socket - pub cores: u32, - - /// Number of threads per core (hyperthreading) - pub threads: u32, - - /// Number of CPU sockets - pub sockets: u32, - - /// CPU frequency in MHz (current or max) - pub frequency_mhz: u32, - - /// Legacy speed field as string (deprecated) - #[deprecated(since = "0.2.0", note = "Use frequency_mhz instead")] - #[serde(skip_serializing_if = "Option::is_none")] - pub speed: Option, - - /// CPU architecture (e.g., "x86_64", "aarch64") - pub architecture: String, - - /// L1 data cache size in kilobytes (per core) - pub cache_l1d_kb: Option, - - /// L1 instruction cache size in kilobytes (per core) - pub cache_l1i_kb: Option, - - /// L2 cache size in kilobytes (per core or shared) - pub cache_l2_kb: Option, - - /// L3 cache size in kilobytes (typically shared) - pub cache_l3_kb: Option, - - /// CPU flags/features (e.g., "avx2", "sve") - pub flags: Vec, - - /// Microcode version - pub microcode_version: Option, -} -``` - ---- - -## Phase 2: Data Gaps - -### 2.1 Memory DIMM Part Number - -See: [MEMORY_ENHANCEMENTS.md](./MEMORY_ENHANCEMENTS.md) - -```rust -/// Individual memory module (DIMM) -/// -/// # References -/// -/// - [JEDEC Memory Standards](https://www.jedec.org/) -/// - [SMBIOS Type 17 Memory Device](https://www.dmtf.org/standards/smbios) -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct MemoryModule { - pub size: String, - pub size_bytes: u64, // NEW - pub type_: String, - pub speed: String, - pub speed_mhz: Option, // NEW - pub location: String, - pub manufacturer: String, - pub serial: String, - pub part_number: Option, // NEW - pub rank: Option, // NEW - pub configured_voltage: Option, // NEW (in volts) -} -``` - -### 2.2 Network Interface Enhancements - -See: [NETWORK_ENHANCEMENTS.md](./NETWORK_ENHANCEMENTS.md) - -```rust -/// Network interface information -/// -/// # References -/// -/// - [Linux Netlink Documentation](https://man7.org/linux/man-pages/man7/netlink.7.html) -/// - [ethtool Source](https://mirrors.edge.kernel.org/pub/software/network/ethtool/) -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct NetworkInterface { - pub name: String, - pub mac: String, - pub ip: String, - pub prefix: String, - pub speed: Option, - pub speed_mbps: Option, // NEW - pub type_: String, - pub vendor: String, - pub model: String, - pub pci_id: String, - pub numa_node: Option, - pub driver: Option, // NEW - pub driver_version: Option, // NEW - pub firmware_version: Option, // NEW - pub mtu: u32, // NEW - pub is_up: bool, // NEW - pub is_virtual: bool, // NEW -} -``` - ---- - -## Phase 3: Runtime Metrics (Optional) - -These are lower priority and may be deferred: - -| Metric | Category | Notes | -|--------|----------|-------| -| GPU temperature | GPU | Requires NVML or sensors | -| GPU utilization | GPU | Requires NVML | -| GPU power draw | GPU | Requires NVML | -| Storage SMART data | Storage | Requires smartctl or sysfs | -| Network statistics | Network | /sys/class/net/*/statistics | - ---- - -## New Dependencies - -### Cargo.toml Changes - -```toml -[dependencies] -# Existing dependencies... -sysinfo = "0.32.0" - -# NEW: NVIDIA GPU detection via NVML -# Optional - requires NVIDIA driver at runtime -nvml-wrapper = { version = "0.9", optional = true } - -# NEW: x86 CPU detection via CPUID -# Only compiled on x86/x86_64 targets -[target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies] -raw-cpuid = { version = "11", optional = true } - -[features] -default = [] -nvidia = ["nvml-wrapper"] -x86-cpu = ["raw-cpuid"] -full = ["nvidia", "x86-cpu"] -``` - -### Dependency Rationale - -| Crate | Purpose | Why Not Shell Out? | -|-------|---------|-------------------| -| `nvml-wrapper` | NVIDIA GPU detection | Direct API access, no parsing, handles errors properly | -| `raw-cpuid` | x86 CPU cache/features | Direct CPU instruction, no external dependencies | -| `sysinfo` | Cross-platform fallback | Already in use, pure Rust | - -### References - -- [nvml-wrapper crate](https://crates.io/crates/nvml-wrapper) - [NVML API Docs](https://docs.nvidia.com/deploy/nvml-api/) -- [raw-cpuid crate](https://crates.io/crates/raw-cpuid) - [Intel CPUID](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) -- [sysinfo crate](https://crates.io/crates/sysinfo) - ---- - -## Implementation Order - -| Step | Task | Priority | Est. Effort | Files Changed | -|------|------|----------|-------------|---------------| -| 1 | Create `StorageType` enum and update `StorageDevice` | Critical | Low | `entities.rs` | -| 2 | Implement sysfs storage detection for Linux | Critical | Medium | `linux.rs`, `storage.rs` | -| 3 | Update `CpuInfo` with frequency/cache fields | Critical | Low | `entities.rs` | -| 4 | Implement sysfs CPU freq/cache detection | Critical | Medium | `linux.rs`, `cpu.rs` | -| 5 | Update `GpuDevice` with numeric memory fields | Critical | Low | `entities.rs` | -| 6 | Implement multi-method GPU detection | Critical | High | `linux.rs`, `gpu.rs` | -| 7 | Add NVML integration (feature-gated) | Critical | Medium | `linux.rs`, `Cargo.toml` | -| 8 | Update `MemoryModule` with part_number | Medium | Low | `entities.rs`, `memory.rs` | -| 9 | Update `NetworkInterface` with driver info | Medium | Medium | `entities.rs`, `linux.rs`, `network.rs` | -| 10 | Add rustdoc to all public items | High | Medium | All files | -| 11 | Add/update tests for ARM and x86 | High | High | `tests/` | -| 12 | Update examples | Medium | Low | `examples/` | - ---- - -## Related Documents - -- [GPU_DETECTION.md](./GPU_DETECTION.md) - Multi-method GPU detection strategy -- [STORAGE_DETECTION.md](./STORAGE_DETECTION.md) - Storage detection with ARM focus -- [CPU_ENHANCEMENTS.md](./CPU_ENHANCEMENTS.md) - CPU frequency and cache detection -- [MEMORY_ENHANCEMENTS.md](./MEMORY_ENHANCEMENTS.md) - Memory module enhancements -- [NETWORK_ENHANCEMENTS.md](./NETWORK_ENHANCEMENTS.md) - Network interface enhancements -- [RUSTDOC_STANDARDS.md](./RUSTDOC_STANDARDS.md) - Documentation standards -- [TESTING_STRATEGY.md](./TESTING_STRATEGY.md) - Testing approach for ARM and x86 - ---- - -## Changelog - -| Date | Version | Changes | -|------|---------|---------| -| 2024-12-29 | 0.2.0-plan | Initial enhancement plan | diff --git a/docs/GPU_DETECTION.md b/docs/GPU_DETECTION.md deleted file mode 100644 index 9b7bc29..0000000 --- a/docs/GPU_DETECTION.md +++ /dev/null @@ -1,955 +0,0 @@ -# GPU Detection Enhancement Plan - -> **Category:** Critical Issue -> **Target Platforms:** Linux (x86_64, aarch64) -> **Related Files:** `src/domain/entities.rs`, `src/adapters/secondary/system/linux.rs`, `src/domain/parsers/gpu.rs` (new) - -## Table of Contents - -1. [Problem Statement](#problem-statement) -2. [Current Implementation](#current-implementation) -3. [Multi-Method Detection Strategy](#multi-method-detection-strategy) -4. [Entity Changes](#entity-changes) -5. [Detection Method Details](#detection-method-details) -6. [Adapter Implementation](#adapter-implementation) -7. [Parser Implementation](#parser-implementation) -8. [Error Handling](#error-handling) -9. [Testing Requirements](#testing-requirements) -10. [References](#references) - ---- - -## Problem Statement - -### Current Issue - -The current GPU memory field returns a formatted string that consumers cannot reliably parse: - -```rust -// Current output -GpuDevice { - memory: "80 GB", // String - cannot parse reliably - // ... -} - -// Consumer code that fails: -let memory_mb = gpu.memory.parse::().unwrap_or(0.0) as u32 * 1024; -// Result: memory_mb = 0 (parse fails on "80 GB") -``` - -### Impact - -- CMDB inventory shows 0MB VRAM for all GPUs -- `metal-agent` must fall back to shelling out to `nvidia-smi` -- No support for AMD or Intel GPUs -- Detection fails silently - -### Requirements - -1. Return numeric memory values in MB (u64) -2. Detect GPUs using multiple methods with fallback chain -3. Support NVIDIA, AMD, and Intel GPUs -4. Work on both x86_64 and aarch64 architectures -5. Provide driver version information -6. Include detection method in output for debugging - ---- - -## Current Implementation - -### Location - -- **Entity:** `src/domain/entities.rs:235-251` -- **Adapter:** `src/adapters/secondary/system/linux.rs:172-231` - -### Current Detection Flow - -``` -┌─────────────────────────────────────────┐ -│ LinuxSystemInfoProvider::get_gpu_info() │ -└─────────────────────────────────────────┘ - │ - ▼ - ┌──────────────────┐ - │ Try nvidia-smi │ - │ --query-gpu=... │ - └──────────────────┘ - │ - success? │ - ┌──────────┴──────────┐ - │ YES │ NO - ▼ ▼ - Parse CSV output ┌──────────────────┐ - Return devices │ Try lspci -nn │ - └──────────────────┘ - │ - Parse VGA/3D lines - Return basic info -``` - -### Current Limitations - -| Limitation | Impact | -|------------|--------| -| Only two detection methods | Misses AMD ROCm, Intel GPUs | -| String memory format | Breaks consumer parsing | -| No driver version | Missing CMDB field | -| No PCI bus ID | Can't correlate with NUMA | -| nvidia-smi parsing fragile | Format changes break detection | - ---- - -## Multi-Method Detection Strategy - -### Detection Priority Chain - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ GPU DETECTION CHAIN │ -│ │ -│ Priority 1: NVML (nvml-wrapper crate) │ -│ ├── Most accurate for NVIDIA GPUs │ -│ ├── Direct API access, no parsing │ -│ ├── Memory in bytes, convert to MB │ -│ └── Feature-gated: #[cfg(feature = "nvidia")] │ -│ │ │ -│ ▼ (if unavailable or no NVIDIA GPUs) │ -│ Priority 2: nvidia-smi command │ -│ ├── Fallback for NVIDIA when NVML unavailable │ -│ ├── Common on systems without development headers │ -│ └── Parse --query-gpu output with nounits flag │ -│ │ │ -│ ▼ (if unavailable or no NVIDIA GPUs) │ -│ Priority 3: ROCm SMI (rocm-smi) │ -│ ├── AMD GPU detection │ -│ ├── Parse JSON output when available │ -│ └── Common on AMD GPU systems │ -│ │ │ -│ ▼ (if unavailable or no AMD GPUs) │ -│ Priority 4: sysfs /sys/class/drm │ -│ ├── Linux DRM subsystem │ -│ ├── Works for all GPU vendors │ -│ ├── Memory info from /sys/class/drm/card*/device/mem_info_* │ -│ └── Vendor from /sys/class/drm/card*/device/vendor │ -│ │ │ -│ ▼ (if no GPUs found) │ -│ Priority 5: lspci with PCI ID database │ -│ ├── Enumerate all VGA/3D controllers │ -│ ├── Look up vendor:device in PCI ID database │ -│ └── No memory info available │ -│ │ │ -│ ▼ (if lspci unavailable) │ -│ Priority 6: sysinfo crate │ -│ ├── Cross-platform fallback │ -│ └── Limited GPU information │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -### Method Capabilities Matrix - -| Method | NVIDIA | AMD | Intel | Memory | Driver | PCI Bus | NUMA | -|--------|--------|-----|-------|--------|--------|---------|------| -| NVML | Yes | No | No | Exact | Yes | Yes | Yes | -| nvidia-smi | Yes | No | No | Exact | Yes | Yes | No | -| rocm-smi | No | Yes | No | Exact | Yes | Yes | No | -| sysfs DRM | Yes | Yes | Yes | Varies | No | Yes | Yes | -| lspci | Yes | Yes | Yes | No | No | Yes | No | -| sysinfo | Limited | Limited | Limited | No | No | No | No | - ---- - -## Entity Changes - -### New GpuDevice Structure - -```rust -// src/domain/entities.rs - -/// GPU vendor classification -/// -/// # References -/// -/// - [PCI Vendor IDs](https://pci-ids.ucw.cz/) -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -pub enum GpuVendor { - /// NVIDIA Corporation (PCI vendor 0x10de) - Nvidia, - /// Advanced Micro Devices (PCI vendor 0x1002) - Amd, - /// Intel Corporation (PCI vendor 0x8086) - Intel, - /// Apple Inc. (integrated GPUs) - Apple, - /// Unknown or unrecognized vendor - Unknown, -} - -impl GpuVendor { - /// Convert PCI vendor ID to GpuVendor - /// - /// # Arguments - /// - /// * `vendor_id` - PCI vendor ID as hexadecimal string (e.g., "10de") - /// - /// # Example - /// - /// ``` - /// use hardware_report::GpuVendor; - /// - /// assert_eq!(GpuVendor::from_pci_vendor("10de"), GpuVendor::Nvidia); - /// assert_eq!(GpuVendor::from_pci_vendor("1002"), GpuVendor::Amd); - /// ``` - pub fn from_pci_vendor(vendor_id: &str) -> Self { - match vendor_id.to_lowercase().as_str() { - "10de" => GpuVendor::Nvidia, - "1002" => GpuVendor::Amd, - "8086" => GpuVendor::Intel, - _ => GpuVendor::Unknown, - } - } -} - -/// GPU device information -/// -/// Represents a discrete or integrated GPU detected in the system. -/// Memory values are provided in megabytes as unsigned integers for -/// reliable parsing by CMDB consumers. -/// -/// # Detection Methods -/// -/// GPUs are detected using multiple methods in priority order: -/// 1. **NVML** - NVIDIA Management Library (most accurate for NVIDIA) -/// 2. **nvidia-smi** - NVIDIA command-line tool (fallback) -/// 3. **rocm-smi** - AMD ROCm System Management Interface -/// 4. **sysfs** - Linux `/sys/class/drm` interface -/// 5. **lspci** - PCI device enumeration -/// 6. **sysinfo** - Cross-platform fallback -/// -/// # Memory Format -/// -/// Memory is always reported in **megabytes** as a `u64`. The previous -/// `memory: String` field (e.g., "80 GB") is deprecated. -/// -/// # Example -/// -/// ``` -/// use hardware_report::GpuDevice; -/// -/// // Calculate memory in GB from the numeric field -/// let memory_gb = gpu.memory_total_mb as f64 / 1024.0; -/// println!("GPU has {} GB memory", memory_gb); -/// ``` -/// -/// # References -/// -/// - [NVIDIA NVML API](https://docs.nvidia.com/deploy/nvml-api/) -/// - [AMD ROCm SMI](https://rocm.docs.amd.com/projects/rocm_smi_lib/en/latest/) -/// - [Linux DRM Subsystem](https://www.kernel.org/doc/html/latest/gpu/drm-uapi.html) -/// - [PCI ID Database](https://pci-ids.ucw.cz/) -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct GpuDevice { - /// GPU index (0-based, unique per system) - pub index: u32, - - /// GPU product name - /// - /// Examples: - /// - "NVIDIA H100 80GB HBM3" - /// - "AMD Instinct MI250X" - /// - "Intel Arc A770" - pub name: String, - - /// GPU UUID (globally unique identifier) - /// - /// Format varies by vendor: - /// - NVIDIA: "GPU-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - /// - AMD: May be empty or use different format - pub uuid: String, - - /// Total GPU memory in megabytes - /// - /// This is the primary memory field and should be used for all - /// programmatic access. Multiply by 1024 for KB, divide by 1024 - /// for GB. - /// - /// # Note - /// - /// Returns 0 if memory could not be determined (e.g., lspci-only detection). - pub memory_total_mb: u64, - - /// Available (free) GPU memory in megabytes - /// - /// This is a runtime value that reflects current memory usage. - /// Returns `None` if not queryable (requires NVML or ROCm). - pub memory_free_mb: Option, - - /// Used GPU memory in megabytes - /// - /// Calculated as `memory_total_mb - memory_free_mb` when available. - pub memory_used_mb: Option, - - /// PCI vendor:device ID (e.g., "10de:2330") - /// - /// Format: `{vendor_id}:{device_id}` in lowercase hexadecimal. - /// - /// # References - /// - /// - [PCI ID Database](https://pci-ids.ucw.cz/) - pub pci_id: String, - - /// PCI bus address (e.g., "0000:01:00.0") - /// - /// Format: `{domain}:{bus}:{device}.{function}` - /// - /// Useful for correlating with NUMA topology and other PCI devices. - pub pci_bus_id: Option, - - /// GPU vendor classification - pub vendor: GpuVendor, - - /// Vendor name as string (e.g., "NVIDIA", "AMD", "Intel") - /// - /// Provided for serialization compatibility. Use `vendor` field - /// for programmatic comparisons. - pub vendor_name: String, - - /// GPU driver version - /// - /// Examples: - /// - NVIDIA: "535.129.03" - /// - AMD: "6.3.6" - pub driver_version: Option, - - /// CUDA compute capability (NVIDIA only) - /// - /// Format: "major.minor" (e.g., "9.0" for Hopper, "8.9" for Ada) - /// - /// # References - /// - /// - [CUDA Compute Capability](https://developer.nvidia.com/cuda-gpus) - pub compute_capability: Option, - - /// GPU architecture name - /// - /// Examples: - /// - NVIDIA: "Hopper", "Ada Lovelace", "Ampere" - /// - AMD: "CDNA2", "RDNA3" - /// - Intel: "Xe-HPG" - pub architecture: Option, - - /// NUMA node affinity - /// - /// The NUMA node this GPU is attached to. Important for optimal - /// CPU-GPU memory transfers. - /// - /// Returns `None` on non-NUMA systems or if not determinable. - pub numa_node: Option, - - /// Power limit in watts (if available) - pub power_limit_watts: Option, - - /// Current temperature in Celsius (if available) - pub temperature_celsius: Option, - - /// Detection method that discovered this GPU - /// - /// One of: "nvml", "nvidia-smi", "rocm-smi", "sysfs", "lspci", "sysinfo" - /// - /// Useful for debugging and understanding data accuracy. - pub detection_method: String, -} - -impl Default for GpuDevice { - fn default() -> Self { - Self { - index: 0, - name: String::new(), - uuid: String::new(), - memory_total_mb: 0, - memory_free_mb: None, - memory_used_mb: None, - pci_id: String::new(), - pci_bus_id: None, - vendor: GpuVendor::Unknown, - vendor_name: "Unknown".to_string(), - driver_version: None, - compute_capability: None, - architecture: None, - numa_node: None, - power_limit_watts: None, - temperature_celsius: None, - detection_method: String::new(), - } - } -} -``` - ---- - -## Detection Method Details - -### Method 1: NVML (nvml-wrapper) - -**When:** Feature `nvidia` enabled, NVIDIA driver installed - -**Pros:** -- Most accurate data -- Direct API, no parsing -- Memory in bytes (exact) -- Full metadata - -**Cons:** -- Requires NVIDIA driver -- NVML library must be present -- NVIDIA GPUs only - -**sysfs paths used:** -- None (direct library calls) - -**References:** -- [nvml-wrapper crate](https://crates.io/crates/nvml-wrapper) -- [NVML API Reference](https://docs.nvidia.com/deploy/nvml-api/) -- [NVML Header](https://github.com/NVIDIA/nvidia-settings/blob/main/src/nvml.h) - ---- - -### Method 2: nvidia-smi - -**When:** NVML unavailable, `nvidia-smi` command available - -**Command:** -```bash -nvidia-smi --query-gpu=index,name,uuid,memory.total,memory.free,pci.bus_id,driver_version,compute_cap --format=csv,noheader,nounits -``` - -**Output format:** -``` -0, NVIDIA H100 80GB HBM3, GPU-xxxx, 81920, 81000, 00000000:01:00.0, 535.129.03, 9.0 -``` - -**Parsing notes:** -- Use `nounits` flag to get numeric values -- Memory is in MiB (mebibytes) -- Fields are comma-separated - -**References:** -- [nvidia-smi Documentation](https://developer.nvidia.com/nvidia-system-management-interface) - ---- - -### Method 3: ROCm SMI - -**When:** AMD GPU detected, `rocm-smi` command available - -**Command:** -```bash -rocm-smi --showproductname --showmeminfo vram --showdriver --json -``` - -**Output format (JSON):** -```json -{ - "card0": { - "Card series": "AMD Instinct MI250X", - "VRAM Total Memory (B)": "137438953472", - "Driver version": "6.3.6" - } -} -``` - -**References:** -- [ROCm SMI Documentation](https://rocm.docs.amd.com/projects/rocm_smi_lib/en/latest/) -- [ROCm GitHub](https://github.com/RadeonOpenCompute/rocm_smi_lib) - ---- - -### Method 4: sysfs DRM - -**When:** Linux, GPUs present in `/sys/class/drm` - -**sysfs paths:** -``` -/sys/class/drm/card{N}/device/ -├── vendor # PCI vendor ID (e.g., "0x10de") -├── device # PCI device ID (e.g., "0x2330") -├── subsystem_vendor # Subsystem vendor ID -├── subsystem_device # Subsystem device ID -├── numa_node # NUMA node affinity -├── mem_info_vram_total # AMD: VRAM total in bytes -├── mem_info_vram_used # AMD: VRAM used in bytes -└── driver/ # Symlink to driver - └── module/ - └── version # Driver version (some drivers) -``` - -**Vendor detection:** -- NVIDIA: vendor = 0x10de -- AMD: vendor = 0x1002 -- Intel: vendor = 0x8086 - -**Memory detection:** -- AMD: `/sys/class/drm/card*/device/mem_info_vram_total` -- Intel: `/sys/class/drm/card*/gt/addr_range` (varies) -- NVIDIA: Not available via sysfs (use NVML) - -**References:** -- [Linux DRM sysfs](https://www.kernel.org/doc/html/latest/gpu/drm-uapi.html) -- [sysfs ABI Documentation](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-drm) - ---- - -### Method 5: lspci - -**When:** Other methods unavailable or for initial enumeration - -**Command:** -```bash -lspci -nn -d ::0300 # VGA compatible controller -lspci -nn -d ::0302 # 3D controller (NVIDIA Tesla/compute) -``` - -**Output format:** -``` -01:00.0 3D controller [0302]: NVIDIA Corporation GH100 [H100 SXM5 80GB] [10de:2330] (rev a1) -``` - -**Parsing:** -- PCI bus ID: `01:00.0` -- Class: `3D controller [0302]` -- Vendor:Device: `[10de:2330]` -- Name: Everything between `:` and `[vendor:device]` - -**References:** -- [lspci man page](https://man7.org/linux/man-pages/man8/lspci.8.html) -- [PCI Class Codes](https://pci-ids.ucw.cz/read/PD/) - ---- - -### Method 6: sysinfo crate - -**When:** Last resort fallback - -**Usage:** -```rust -use sysinfo::System; - -let sys = System::new_all(); -// sysinfo doesn't currently expose GPU info -// but may in future versions -``` - -**Current limitation:** sysinfo does not expose GPU information as of v0.32. - -**References:** -- [sysinfo crate](https://crates.io/crates/sysinfo) - ---- - -## Adapter Implementation - -### File: `src/adapters/secondary/system/linux.rs` - -```rust -// Pseudocode for new implementation - -impl SystemInfoProvider for LinuxSystemInfoProvider { - async fn get_gpu_info(&self) -> Result { - let mut devices = Vec::new(); - - // Method 1: Try NVML (feature-gated) - #[cfg(feature = "nvidia")] - { - if let Ok(nvml_gpus) = self.detect_gpus_nvml().await { - devices.extend(nvml_gpus); - } - } - - // Method 2: Try nvidia-smi (if no NVML results) - if devices.is_empty() { - if let Ok(smi_gpus) = self.detect_gpus_nvidia_smi().await { - devices.extend(smi_gpus); - } - } - - // Method 3: Try rocm-smi for AMD - if let Ok(rocm_gpus) = self.detect_gpus_rocm_smi().await { - devices.extend(rocm_gpus); - } - - // Method 4: Try sysfs DRM - if let Ok(drm_gpus) = self.detect_gpus_sysfs_drm().await { - // Merge with existing or add new - self.merge_gpu_info(&mut devices, drm_gpus); - } - - // Method 5: Try lspci (for devices not yet found) - if let Ok(pci_gpus) = self.detect_gpus_lspci().await { - // Merge with existing or add new - self.merge_gpu_info(&mut devices, pci_gpus); - } - - // Enrich with NUMA info - self.enrich_gpu_numa_info(&mut devices).await; - - // Re-index - for (i, gpu) in devices.iter_mut().enumerate() { - gpu.index = i as u32; - } - - Ok(GpuInfo { devices }) - } -} -``` - -### Helper Methods - -```rust -impl LinuxSystemInfoProvider { - /// Detect GPUs using NVML library - /// - /// # Requirements - /// - /// - Feature `nvidia` must be enabled - /// - NVIDIA driver must be installed - /// - NVML library must be loadable - #[cfg(feature = "nvidia")] - async fn detect_gpus_nvml(&self) -> Result, SystemError> { - // Implementation using nvml-wrapper - todo!() - } - - /// Detect GPUs using nvidia-smi command - /// - /// # Requirements - /// - /// - `nvidia-smi` must be in PATH - /// - NVIDIA driver must be installed - async fn detect_gpus_nvidia_smi(&self) -> Result, SystemError> { - // Implementation using command execution - todo!() - } - - /// Detect AMD GPUs using rocm-smi command - /// - /// # Requirements - /// - /// - `rocm-smi` must be in PATH - /// - ROCm must be installed - async fn detect_gpus_rocm_smi(&self) -> Result, SystemError> { - // Implementation using command execution - todo!() - } - - /// Detect GPUs using sysfs DRM interface - /// - /// # References - /// - /// - [sysfs DRM ABI](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-drm) - async fn detect_gpus_sysfs_drm(&self) -> Result, SystemError> { - // Implementation reading /sys/class/drm - todo!() - } - - /// Detect GPUs using lspci command - /// - /// # Requirements - /// - /// - `lspci` must be in PATH (pciutils package) - async fn detect_gpus_lspci(&self) -> Result, SystemError> { - // Implementation using command execution - todo!() - } - - /// Merge GPU info from multiple sources - /// - /// GPUs are matched by PCI bus ID. Information from higher-priority - /// sources takes precedence, but missing fields are filled in from - /// lower-priority sources. - fn merge_gpu_info(&self, primary: &mut Vec, secondary: Vec) { - // Implementation - todo!() - } - - /// Enrich GPU devices with NUMA node information - /// - /// Reads NUMA affinity from `/sys/class/drm/card{N}/device/numa_node` - async fn enrich_gpu_numa_info(&self, devices: &mut [GpuDevice]) { - // Implementation - todo!() - } -} -``` - ---- - -## Parser Implementation - -### New File: `src/domain/parsers/gpu.rs` - -```rust -//! GPU information parsing functions -//! -//! This module provides pure parsing functions for GPU information from -//! various sources. All functions take string input and return parsed -//! results without performing I/O. -//! -//! # Supported Formats -//! -//! - nvidia-smi CSV output -//! - rocm-smi JSON output -//! - lspci text output -//! - sysfs file contents -//! -//! # Example -//! -//! ``` -//! use hardware_report::domain::parsers::gpu::parse_nvidia_smi_output; -//! -//! let output = "0, NVIDIA H100, GPU-xxx, 81920, 81000, 00:01:00.0, 535.129.03, 9.0"; -//! let gpus = parse_nvidia_smi_output(output).unwrap(); -//! assert_eq!(gpus[0].memory_total_mb, 81920); -//! ``` - -use crate::domain::{GpuDevice, GpuVendor}; - -/// Parse nvidia-smi CSV output into GPU devices -/// -/// # Arguments -/// -/// * `output` - Output from `nvidia-smi --query-gpu=... --format=csv,noheader,nounits` -/// -/// # Expected Format -/// -/// ```text -/// index, name, uuid, memory.total, memory.free, pci.bus_id, driver_version, compute_cap -/// 0, NVIDIA H100 80GB HBM3, GPU-xxxx, 81920, 81000, 00000000:01:00.0, 535.129.03, 9.0 -/// ``` -/// -/// # Errors -/// -/// Returns an error if the output format is invalid or cannot be parsed. -/// -/// # References -/// -/// - [nvidia-smi Query Options](https://developer.nvidia.com/nvidia-system-management-interface) -pub fn parse_nvidia_smi_output(output: &str) -> Result, String> { - todo!() -} - -/// Parse rocm-smi JSON output into GPU devices -/// -/// # Arguments -/// -/// * `output` - JSON output from `rocm-smi --json` -/// -/// # Expected Format -/// -/// ```json -/// { -/// "card0": { -/// "Card series": "AMD Instinct MI250X", -/// "VRAM Total Memory (B)": "137438953472", -/// "Driver version": "6.3.6" -/// } -/// } -/// ``` -/// -/// # Errors -/// -/// Returns an error if the JSON is invalid or required fields are missing. -/// -/// # References -/// -/// - [ROCm SMI Documentation](https://rocm.docs.amd.com/projects/rocm_smi_lib/en/latest/) -pub fn parse_rocm_smi_output(output: &str) -> Result, String> { - todo!() -} - -/// Parse lspci output for GPU devices -/// -/// # Arguments -/// -/// * `output` - Output from `lspci -nn` -/// -/// # Expected Format -/// -/// ```text -/// 01:00.0 3D controller [0302]: NVIDIA Corporation GH100 [H100 SXM5 80GB] [10de:2330] (rev a1) -/// ``` -/// -/// # Note -/// -/// This method cannot determine GPU memory. The `memory_total_mb` field -/// will be set to 0 for GPUs detected via lspci only. -/// -/// # References -/// -/// - [lspci man page](https://man7.org/linux/man-pages/man8/lspci.8.html) -pub fn parse_lspci_gpu_output(output: &str) -> Result, String> { - todo!() -} - -/// Parse PCI vendor ID to determine GPU vendor -/// -/// # Arguments -/// -/// * `vendor_id` - PCI vendor ID in hexadecimal (e.g., "10de", "0x10de") -/// -/// # Returns -/// -/// The corresponding `GpuVendor` enum value. -/// -/// # Example -/// -/// ``` -/// use hardware_report::domain::parsers::gpu::parse_pci_vendor; -/// use hardware_report::GpuVendor; -/// -/// assert_eq!(parse_pci_vendor("10de"), GpuVendor::Nvidia); -/// assert_eq!(parse_pci_vendor("0x1002"), GpuVendor::Amd); -/// ``` -/// -/// # References -/// -/// - [PCI Vendor IDs](https://pci-ids.ucw.cz/) -pub fn parse_pci_vendor(vendor_id: &str) -> GpuVendor { - todo!() -} - -/// Parse sysfs DRM memory info for AMD GPUs -/// -/// # Arguments -/// -/// * `content` - Content of `/sys/class/drm/card*/device/mem_info_vram_total` -/// -/// # Returns -/// -/// Memory size in megabytes. -/// -/// # References -/// -/// - [AMDGPU sysfs](https://www.kernel.org/doc/html/latest/gpu/amdgpu/driver-misc.html) -pub fn parse_sysfs_vram_total(content: &str) -> Result { - todo!() -} -``` - ---- - -## Error Handling - -### Error Types - -```rust -/// GPU detection-specific errors -#[derive(Debug, thiserror::Error)] -pub enum GpuDetectionError { - /// NVML library initialization failed - #[error("NVML initialization failed: {0}")] - NvmlInitFailed(String), - - /// No GPUs found by any method - #[error("No GPUs detected")] - NoGpusFound, - - /// Command execution failed - #[error("GPU detection command failed: {command}: {reason}")] - CommandFailed { - command: String, - reason: String, - }, - - /// Output parsing failed - #[error("Failed to parse GPU info from {source}: {reason}")] - ParseFailed { - source: String, - reason: String, - }, - - /// sysfs read failed - #[error("Failed to read sysfs path {path}: {reason}")] - SysfsFailed { - path: String, - reason: String, - }, -} -``` - -### Error Handling Strategy - -1. **Never fail completely** - Return partial results if some methods work -2. **Log warnings** - Log failures at detection methods for debugging -3. **Include detection_method** - So consumers know data accuracy -4. **Return empty GpuInfo** - If no GPUs found (not an error condition) - ---- - -## Testing Requirements - -### Unit Tests - -| Test | Description | -|------|-------------| -| `test_parse_nvidia_smi_output` | Parse valid nvidia-smi CSV | -| `test_parse_nvidia_smi_empty` | Handle empty nvidia-smi output | -| `test_parse_rocm_smi_output` | Parse valid rocm-smi JSON | -| `test_parse_lspci_output` | Parse lspci with multiple GPUs | -| `test_parse_pci_vendor` | Vendor ID to enum conversion | -| `test_gpu_merge` | Merging info from multiple sources | - -### Integration Tests - -| Test | Platform | Description | -|------|----------|-------------| -| `test_gpu_detection_nvidia` | x86_64 + NVIDIA | Full detection with real GPU | -| `test_gpu_detection_amd` | x86_64 + AMD | Full detection with AMD GPU | -| `test_gpu_detection_arm` | aarch64 | Detection on ARM (DGX Spark) | -| `test_gpu_detection_no_gpu` | Any | Graceful handling of no GPU | - -### Test Hardware Matrix - -| Platform | GPU | Test Target | -|----------|-----|-------------| -| x86_64 Linux | NVIDIA H100 | CI + Manual | -| x86_64 Linux | AMD MI250X | Manual | -| aarch64 Linux | NVIDIA (DGX Spark) | Manual | -| aarch64 Linux | No GPU | CI | - ---- - -## References - -### Official Documentation - -| Resource | URL | -|----------|-----| -| NVIDIA NVML API | https://docs.nvidia.com/deploy/nvml-api/ | -| NVIDIA SMI | https://developer.nvidia.com/nvidia-system-management-interface | -| AMD ROCm SMI | https://rocm.docs.amd.com/projects/rocm_smi_lib/en/latest/ | -| Linux DRM | https://www.kernel.org/doc/html/latest/gpu/drm-uapi.html | -| PCI ID Database | https://pci-ids.ucw.cz/ | -| CUDA Compute Capability | https://developer.nvidia.com/cuda-gpus | - -### Crate Documentation - -| Crate | URL | -|-------|-----| -| nvml-wrapper | https://docs.rs/nvml-wrapper | -| sysinfo | https://docs.rs/sysinfo | - -### Kernel Documentation - -| Path | Description | -|------|-------------| -| `/sys/class/drm/` | DRM subsystem sysfs | -| `/sys/class/drm/card*/device/vendor` | PCI vendor ID | -| `/sys/class/drm/card*/device/numa_node` | NUMA affinity | - ---- - -## Changelog - -| Date | Changes | -|------|---------| -| 2024-12-29 | Initial specification | diff --git a/docs/IMPLEMENTATION_GUIDE.md b/docs/IMPLEMENTATION_GUIDE.md deleted file mode 100644 index 1c73ee8..0000000 --- a/docs/IMPLEMENTATION_GUIDE.md +++ /dev/null @@ -1,3679 +0,0 @@ -# Implementation Guide: Learn by Doing - -> **Purpose:** Step-by-step implementation guide with LeetCode patterns and real-world connections -> **Learning Style:** Type it yourself with detailed explanations - -## Table of Contents - -1. [Overview](#overview) -2. [LeetCode Patterns Used](#leetcode-patterns-used) -3. [Implementation Order](#implementation-order) -4. [Step 1: Storage Enhancements](#step-1-storage-enhancements) -5. [Step 2: CPU Enhancements](#step-2-cpu-enhancements) -6. [Step 3: GPU Enhancements](#step-3-gpu-enhancements) -7. [Step 4: Memory Enhancements](#step-4-memory-enhancements) -8. [Step 5: Network Enhancements](#step-5-network-enhancements) -9. [Step 6: Cargo.toml Updates](#step-6-cargotoml-updates) - ---- - -## Overview - -This guide walks you through implementing each enhancement with: -- **Commented code** explaining every decision -- **LeetCode pattern callouts** showing real-world applications -- **Why it matters** for CMDB/inventory systems - -### How to Use This Guide - -1. Read each section's explanation -2. Type the code yourself (don't copy-paste!) -3. Run `cargo check` after each change -4. Run `cargo test` to verify -5. Understand the LeetCode pattern connection - ---- - -## LeetCode Patterns Used - -This project uses several classic algorithm patterns. Here's how they map: - -| Pattern | LeetCode Examples | Where Used Here | -|---------|-------------------|-----------------| -| **Chain of Responsibility** | - | Multi-method detection (try method 1, fallback to 2, etc.) | -| **Strategy Pattern** | - | Different parsers for different data sources | -| **Builder Pattern** | - | Constructing complex structs with defaults | -| **Two Pointers / Sliding Window** | LC #3, #76, #567 | Parsing delimited strings | -| **Hash Map for Lookups** | LC #1, #49, #242 | PCI vendor ID → vendor name mapping | -| **Tree/Graph Traversal** | LC #200, #547 | Walking sysfs directory tree | -| **String Parsing** | LC #8, #65, #468 | Parsing nvidia-smi output, sysfs files | -| **Merge/Combine Data** | LC #56, #88 | Merging GPU info from multiple sources | -| **Filter/Transform** | LC #283, #27 | Filtering virtual devices, transforming sizes | -| **State Machine** | LC #65, #10 | Parsing multi-line dmidecode output | -| **Adapter Pattern** | - | Platform-specific implementations behind traits | - ---- - -## Implementation Order - -Follow this exact order to avoid compilation errors: - -``` -1. entities.rs - Add new types (StorageType, GpuVendor, etc.) -2. parsers/*.rs - Add parsing functions (pure, no I/O) -3. linux.rs - Update adapter to use new parsers -4. Cargo.toml - Add new dependencies (if needed) -5. tests - Verify everything works -``` - -**Why this order?** -- Entities are dependencies for everything else -- Parsers depend only on entities (pure functions) -- Adapters depend on both entities and parsers -- Tests depend on all of the above - ---- - -## Step 1: Storage Enhancements - -### 1.1 Add StorageType Enum to entities.rs - -**File:** `src/domain/entities.rs` - -**Where:** Add after line 205 (after MemoryModule), before StorageInfo - -**LeetCode Pattern:** This is similar to **categorization problems** like LC #49 (Group Anagrams) -where you classify items into buckets. Here we classify storage devices by type. - -```rust -// ============================================================================= -// STORAGE TYPE ENUM -// ============================================================================= -// -// WHY: We need to categorize storage devices so CMDB consumers can: -// 1. Filter by type (show only SSDs) -// 2. Calculate capacity by category -// 3. Apply different monitoring thresholds -// -// LEETCODE CONNECTION: This is the "categorization" pattern seen in: -// - LC #49 Group Anagrams: group strings by sorted chars -// - LC #347 Top K Frequent: group by frequency -// - Here: group storage by technology type -// -// PATTERN: Enum with associated functions for classification -// ============================================================================= - -/// Storage device type classification. -/// -/// Classifies storage devices by their underlying technology and interface. -/// This enables filtering, capacity planning, and performance expectations. -/// -/// # Detection Logic -/// -/// Type is determined by examining (in order): -/// 1. Device name prefix (`nvme*` → NVMe, `mmcblk*` → eMMC) -/// 2. sysfs rotational flag (`0` = solid state, `1` = spinning) -/// 3. Interface type from sysfs -/// -/// # Example -/// -/// ```rust -/// use hardware_report::StorageType; -/// -/// // Classify based on device name and rotational flag -/// let nvme = StorageType::from_device("nvme0n1", false); -/// assert_eq!(nvme, StorageType::Nvme); -/// -/// let hdd = StorageType::from_device("sda", true); // rotational=1 -/// assert_eq!(hdd, StorageType::Hdd); -/// -/// let ssd = StorageType::from_device("sda", false); // rotational=0 -/// assert_eq!(ssd, StorageType::Ssd); -/// ``` -/// -/// # References -/// -/// - [Linux Block Devices](https://www.kernel.org/doc/html/latest/block/index.html) -/// - [NVMe Specification](https://nvmexpress.org/specifications/) -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -pub enum StorageType { - /// NVMe solid-state drive (PCIe interface). - /// - /// Highest performance storage. Detected by `nvme*` device name prefix. - /// Uses PCIe lanes directly, bypassing SATA/SAS bottlenecks. - Nvme, - - /// SATA/SAS solid-state drive. - /// - /// Detected by `rotational=0` on `sd*` devices. - /// Limited by SATA (6 Gbps) or SAS (12/24 Gbps) interface. - Ssd, - - /// Hard disk drive (rotational/spinning media). - /// - /// Detected by `rotational=1` on `sd*` devices. - /// Mechanical seek time limits random I/O performance. - Hdd, - - /// Embedded MMC storage. - /// - /// Common on ARM platforms (Raspberry Pi, embedded systems). - /// Detected by `mmcblk*` device name prefix. - Emmc, - - /// Virtual or memory-backed device. - /// - /// Includes loop devices, RAM disks, device-mapper. - /// Usually filtered out for hardware inventory. - Virtual, - - /// Unknown or unclassified storage type. - Unknown, -} - -// ============================================================================= -// IMPLEMENTATION: StorageType classification logic -// ============================================================================= -// -// LEETCODE CONNECTION: This classification logic is similar to: -// - LC #68 Text Justification: pattern matching on input -// - LC #722 Remove Comments: state-based string analysis -// -// The pattern here is: examine input characteristics → map to category -// ============================================================================= - -impl StorageType { - /// Determine storage type from device name and rotational flag. - /// - /// This implements a decision tree: - /// ```text - /// device_name - /// │ - /// ┌───────────────┼───────────────┐ - /// ▼ ▼ ▼ - /// nvme* mmcblk* other - /// │ │ │ - /// ▼ ▼ ▼ - /// Nvme Emmc is_rotational? - /// │ - /// ┌────────┴────────┐ - /// ▼ ▼ - /// true false - /// │ │ - /// ▼ ▼ - /// Hdd Ssd - /// ``` - /// - /// # Arguments - /// - /// * `device_name` - Block device name (e.g., "nvme0n1", "sda", "mmcblk0") - /// * `is_rotational` - Whether device uses rotational media (from sysfs) - /// - /// # Why This Order Matters - /// - /// We check name prefixes FIRST because: - /// 1. NVMe devices always report rotational=0, but we want specific type - /// 2. eMMC devices may not have rotational flag - /// 3. Name-based detection is most reliable - pub fn from_device(device_name: &str, is_rotational: bool) -> Self { - // STEP 1: Check for NVMe (highest priority, most specific) - // NVMe devices are named nvme{controller}n{namespace} - // Example: nvme0n1, nvme1n1 - if device_name.starts_with("nvme") { - return StorageType::Nvme; - } - - // STEP 2: Check for eMMC (common on ARM) - // eMMC devices are named mmcblk{N} - // Example: mmcblk0, mmcblk1 - if device_name.starts_with("mmcblk") { - return StorageType::Emmc; - } - - // STEP 3: Check for virtual devices (filter these out usually) - // These are not physical hardware - if device_name.starts_with("loop") // Loop devices (ISO mounts, etc.) - || device_name.starts_with("ram") // RAM disks - || device_name.starts_with("dm-") // Device mapper (LVM, LUKS) - || device_name.starts_with("zram") // Compressed RAM swap - || device_name.starts_with("nbd") // Network block device - { - return StorageType::Virtual; - } - - // STEP 4: For sd* and vd* devices, use rotational flag - // sd* = SCSI/SATA/SAS devices - // vd* = VirtIO devices (VMs) - if is_rotational { - StorageType::Hdd - } else if device_name.starts_with("sd") || device_name.starts_with("vd") { - StorageType::Ssd - } else { - StorageType::Unknown - } - } - - /// Get human-readable display name. - /// - /// Useful for CLI output and logging. - pub fn display_name(&self) -> &'static str { - match self { - StorageType::Nvme => "NVMe SSD", - StorageType::Ssd => "SSD", - StorageType::Hdd => "HDD", - StorageType::Emmc => "eMMC", - StorageType::Virtual => "Virtual", - StorageType::Unknown => "Unknown", - } - } - - /// Check if this is a solid-state device (no moving parts). - /// - /// Useful for performance expectations and wear-leveling considerations. - pub fn is_solid_state(&self) -> bool { - matches!(self, StorageType::Nvme | StorageType::Ssd | StorageType::Emmc) - } -} - -// Implement Display trait for easy printing -// This allows: println!("{}", storage_type); -impl std::fmt::Display for StorageType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.display_name()) - } -} -``` - -### 1.2 Update StorageDevice Struct - -**File:** `src/domain/entities.rs` - -**Where:** Replace the existing `StorageDevice` struct (around line 214-225) - -**LeetCode Pattern:** This uses the **Builder Pattern** concept where we have -many optional fields with sensible defaults. Similar to how LC #146 LRU Cache -needs to track multiple pieces of state for each entry. - -```rust -// ============================================================================= -// STORAGE DEVICE STRUCT -// ============================================================================= -// -// WHY: The old struct had: -// - type_: String → Hard to filter/compare -// - size: String → "500 GB" can't be summed or compared -// - No serial/firmware for asset tracking -// -// NEW: We add: -// - device_type: StorageType enum → Easy filtering -// - size_bytes: u64 → Math works! -// - serial_number, firmware_version → Asset tracking -// -// LEETCODE CONNECTION: This is like the "design" problems: -// - LC #146 LRU Cache: track multiple attributes per entry -// - LC #380 Insert Delete GetRandom: need efficient lookups -// - Here: need efficient queries by type, size, serial -// ============================================================================= - -/// Storage device information. -/// -/// Represents a block storage device with comprehensive metadata for -/// CMDB inventory, capacity planning, and asset tracking. -/// -/// # Detection Methods -/// -/// Storage devices are detected using multiple methods (Chain of Responsibility): -/// 1. **sysfs** `/sys/block` - Primary, most reliable on Linux -/// 2. **lsblk** - Structured command output -/// 3. **nvme-cli** - NVMe-specific details -/// 4. **sysinfo** - Cross-platform fallback -/// 5. **smartctl** - SMART data enrichment -/// -/// # Size Fields -/// -/// Size is provided in multiple formats for convenience: -/// - `size_bytes` - Raw bytes (use for calculations) -/// - `size_gb` - Gigabytes as float (use for display) -/// - `size_tb` - Terabytes as float (use for large arrays) -/// -/// # Example -/// -/// ```rust -/// use hardware_report::{StorageDevice, StorageType}; -/// -/// let device = StorageDevice { -/// name: "nvme0n1".to_string(), -/// device_type: StorageType::Nvme, -/// size_bytes: 2_000_398_934_016, // ~2TB -/// ..Default::default() -/// }; -/// -/// // Calculate total across devices -/// let devices = vec![device]; -/// let total_tb: f64 = devices.iter() -/// .map(|d| d.size_bytes as f64) -/// .sum::() / (1024.0_f64.powi(4)); -/// ``` -/// -/// # References -/// -/// - [Linux sysfs block](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-block) -/// - [NVMe CLI](https://github.com/linux-nvme/nvme-cli) -/// - [smartmontools](https://www.smartmontools.org/) -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct StorageDevice { - // ========================================================================= - // IDENTIFICATION FIELDS - // ========================================================================= - - /// Block device name without /dev/ prefix. - /// - /// Examples: "nvme0n1", "sda", "mmcblk0" - /// - /// This is the kernel's name for the device, found in /sys/block/ - pub name: String, - - /// Full device path. - /// - /// Example: "/dev/nvme0n1" - /// - /// Use this when you need to open/read the device. - #[serde(default)] - pub device_path: String, - - // ========================================================================= - // TYPE AND CLASSIFICATION - // ========================================================================= - - /// Storage device type classification. - /// - /// Use this for filtering and categorization. - /// This is the NEW preferred field. - #[serde(default)] - pub device_type: StorageType, - - /// Legacy type field as string. - /// - /// DEPRECATED: Use `device_type` instead. - /// Kept for backward compatibility with existing consumers. - #[serde(rename = "type")] - pub type_: String, - - // ========================================================================= - // SIZE FIELDS - // ========================================================================= - // - // LEETCODE CONNECTION: Having multiple representations is like - // LC #273 Integer to English Words - same data, different formats - // ========================================================================= - - /// Device size in bytes. - /// - /// PRIMARY SIZE FIELD - use this for calculations. - /// - /// Calculated from sysfs: sectors × 512 (sector size) - #[serde(default)] - pub size_bytes: u64, - - /// Device size in gigabytes (binary, 1 GB = 1024³ bytes). - /// - /// Convenience field for display. Pre-calculated from size_bytes. - #[serde(default)] - pub size_gb: f64, - - /// Device size in terabytes (binary, 1 TB = 1024⁴ bytes). - /// - /// Convenience field for large storage arrays. - #[serde(default)] - pub size_tb: f64, - - /// Legacy size as human-readable string. - /// - /// DEPRECATED: Use `size_bytes` for calculations. - /// Example: "2 TB", "500 GB" - pub size: String, - - // ========================================================================= - // HARDWARE IDENTIFICATION - // ========================================================================= - - /// Device model name. - /// - /// From sysfs `/sys/block/{dev}/device/model` - /// May have trailing whitespace (hardware quirk). - /// - /// Example: "Samsung SSD 980 PRO 2TB" - pub model: String, - - /// Device serial number. - /// - /// IMPORTANT for asset tracking and warranty. - /// - /// May require elevated privileges to read. - /// Sources: - /// - sysfs: `/sys/block/{dev}/device/serial` - /// - NVMe: `/sys/class/nvme/{ctrl}/serial` - /// - smartctl: `smartctl -i /dev/{dev}` - #[serde(default, skip_serializing_if = "Option::is_none")] - pub serial_number: Option, - - /// Device firmware version. - /// - /// IMPORTANT for compliance and update tracking. - /// - /// Sources: - /// - sysfs: `/sys/block/{dev}/device/firmware_rev` - /// - NVMe: `/sys/class/nvme/{ctrl}/firmware_rev` - #[serde(default, skip_serializing_if = "Option::is_none")] - pub firmware_version: Option, - - // ========================================================================= - // INTERFACE AND TRANSPORT - // ========================================================================= - - /// Interface type. - /// - /// Examples: "NVMe", "SATA", "SAS", "USB", "eMMC", "virtio" - #[serde(default)] - pub interface: String, - - /// Whether device uses rotational media. - /// - /// - `true` = HDD (spinning platters, mechanical seek) - /// - `false` = SSD/NVMe (solid state, no moving parts) - /// - /// From sysfs: `/sys/block/{dev}/queue/rotational` - #[serde(default)] - pub is_rotational: bool, - - /// World Wide Name (globally unique identifier). - /// - /// More persistent than serial in some cases. - /// Format varies by protocol (NAA, EUI-64, NGUID). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub wwn: Option, - - // ========================================================================= - // NVME-SPECIFIC FIELDS - // ========================================================================= - - /// NVMe namespace ID (NVMe devices only). - /// - /// Identifies the namespace within the NVMe controller. - /// Most consumer drives have a single namespace (1). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub nvme_namespace: Option, - - // ========================================================================= - // HEALTH AND MONITORING - // ========================================================================= - - /// SMART health status. - /// - /// Values: "PASSED", "FAILED", or None if unavailable. - /// Requires smartctl or NVMe health query. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub smart_status: Option, - - // ========================================================================= - // BLOCK SIZE INFORMATION - // ========================================================================= - // - // LEETCODE CONNECTION: Block sizes matter for alignment, similar to - // LC #68 Text Justification where you need proper boundaries - // ========================================================================= - - /// Logical block size in bytes. - /// - /// Typically 512 or 4096. Affects I/O alignment. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub logical_block_size: Option, - - /// Physical block size in bytes. - /// - /// May differ from logical (512e drives report 512 logical, 4096 physical). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub physical_block_size: Option, - - // ========================================================================= - // METADATA - // ========================================================================= - - /// Detection method that discovered this device. - /// - /// Values: "sysfs", "lsblk", "nvme-cli", "sysinfo", "smartctl" - /// - /// Useful for debugging and understanding data quality. - #[serde(default)] - pub detection_method: String, -} - -// ============================================================================= -// DEFAULT IMPLEMENTATION -// ============================================================================= -// -// LEETCODE CONNECTION: Default/Builder pattern is used in many design problems -// LC #146 LRU Cache, LC #355 Design Twitter - initialize with sensible defaults -// ============================================================================= - -impl Default for StorageType { - fn default() -> Self { - StorageType::Unknown - } -} - -impl Default for StorageDevice { - fn default() -> Self { - Self { - name: String::new(), - device_path: String::new(), - device_type: StorageType::Unknown, - type_: String::new(), - size_bytes: 0, - size_gb: 0.0, - size_tb: 0.0, - size: String::new(), - model: String::new(), - serial_number: None, - firmware_version: None, - interface: "Unknown".to_string(), - is_rotational: false, - wwn: None, - nvme_namespace: None, - smart_status: None, - logical_block_size: None, - physical_block_size: None, - detection_method: String::new(), - } - } -} - -impl StorageDevice { - /// Calculate size_gb and size_tb from size_bytes. - /// - /// Call this after setting size_bytes to populate convenience fields. - /// - /// # Example - /// - /// ```rust - /// let mut device = StorageDevice::default(); - /// device.size_bytes = 1_000_000_000_000; // 1 TB in bytes - /// device.calculate_size_fields(); - /// assert!((device.size_gb - 931.32).abs() < 0.01); // Binary GB - /// ``` - pub fn calculate_size_fields(&mut self) { - // Use binary units (1024-based) as is standard for storage - const KB: f64 = 1024.0; - const GB: f64 = KB * KB * KB; // 1,073,741,824 - const TB: f64 = KB * KB * KB * KB; // 1,099,511,627,776 - - self.size_gb = self.size_bytes as f64 / GB; - self.size_tb = self.size_bytes as f64 / TB; - - // Also set the legacy string field - if self.size_tb >= 1.0 { - self.size = format!("{:.2} TB", self.size_tb); - } else if self.size_gb >= 1.0 { - self.size = format!("{:.2} GB", self.size_gb); - } else { - self.size = format!("{} bytes", self.size_bytes); - } - } - - /// Create device path from name. - /// - /// Convenience method to set device_path from name. - pub fn set_device_path(&mut self) { - if !self.name.is_empty() && self.device_path.is_empty() { - self.device_path = format!("/dev/{}", self.name); - } - } -} -``` - -### 1.3 Add Storage Parser Functions - -**File:** `src/domain/parsers/storage.rs` - -**Where:** Add these functions to the existing file - -**LeetCode Pattern:** String parsing here is like LC #8 (String to Integer), -LC #468 (Validate IP Address), and LC #65 (Valid Number) - parsing structured -text with edge cases. - -```rust -// ============================================================================= -// STORAGE PARSER FUNCTIONS -// ============================================================================= -// -// These are PURE FUNCTIONS - they take strings in, return parsed data out. -// No I/O, no side effects. This makes them easy to test. -// -// ARCHITECTURE: These live in the DOMAIN layer (ports and adapters pattern) -// The ADAPTER layer (linux.rs) calls these after reading from sysfs/commands. -// ============================================================================= - -use crate::domain::{StorageDevice, StorageType}; - -// ============================================================================= -// SYSFS SIZE PARSING -// ============================================================================= -// -// LEETCODE CONNECTION: This is classic string-to-number parsing like: -// - LC #8 String to Integer (atoi) -// - LC #7 Reverse Integer -// -// Key insight: sysfs reports sizes in 512-byte SECTORS, not bytes! -// ============================================================================= - -/// Parse sysfs size file to bytes. -/// -/// The Linux kernel reports block device sizes in 512-byte sectors, -/// regardless of the actual hardware sector size. -/// -/// # Arguments -/// -/// * `content` - Content of `/sys/block/{dev}/size` file -/// -/// # Returns -/// -/// Size in bytes as u64. -/// -/// # Formula -/// -/// ```text -/// size_bytes = sectors × 512 -/// ``` -/// -/// # Example -/// -/// ```rust -/// use hardware_report::domain::parsers::storage::parse_sysfs_size; -/// -/// // A 2TB drive has approximately 3.9 billion sectors -/// let size = parse_sysfs_size("3907029168").unwrap(); -/// assert_eq!(size, 3907029168 * 512); // ~2TB -/// -/// // Handle whitespace (sysfs files often have trailing newline) -/// let size = parse_sysfs_size("1000000\n").unwrap(); -/// assert_eq!(size, 1000000 * 512); -/// ``` -/// -/// # Errors -/// -/// Returns error if content is not a valid integer. -/// -/// # References -/// -/// - [sysfs block size](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-block) -pub fn parse_sysfs_size(content: &str) -> Result { - // STEP 1: Trim whitespace (sysfs files have trailing newlines) - let trimmed = content.trim(); - - // STEP 2: Parse as u64 - // Using parse::() which handles the conversion - let sectors: u64 = trimmed - .parse() - .map_err(|e| format!("Failed to parse sector count '{}': {}", trimmed, e))?; - - // STEP 3: Convert sectors to bytes - // Kernel ALWAYS uses 512-byte sectors for this file - const SECTOR_SIZE: u64 = 512; - Ok(sectors * SECTOR_SIZE) -} - -// ============================================================================= -// ROTATIONAL FLAG PARSING -// ============================================================================= -// -// LEETCODE CONNECTION: Simple boolean parsing, but demonstrates -// defensive programming - handle unexpected inputs gracefully. -// ============================================================================= - -/// Parse sysfs rotational flag. -/// -/// # Arguments -/// -/// * `content` - Content of `/sys/block/{dev}/queue/rotational` -/// -/// # Returns -/// -/// - `true` if device is rotational (HDD) -/// - `false` if solid-state (SSD, NVMe) -/// -/// # Why This Matters -/// -/// Rotational devices have: -/// - Mechanical seek latency (milliseconds vs microseconds) -/// - Sequential access is much faster than random -/// - Different SMART attributes -/// -/// # Example -/// -/// ```rust -/// use hardware_report::domain::parsers::storage::parse_sysfs_rotational; -/// -/// assert!(parse_sysfs_rotational("1")); // HDD -/// assert!(!parse_sysfs_rotational("0")); // SSD -/// assert!(!parse_sysfs_rotational("0\n")); // With newline -/// assert!(!parse_sysfs_rotational("")); // Empty = assume SSD -/// ``` -pub fn parse_sysfs_rotational(content: &str) -> bool { - // Only "1" means rotational; anything else (0, empty, error) = non-rotational - content.trim() == "1" -} - -// ============================================================================= -// LSBLK JSON PARSING -// ============================================================================= -// -// LEETCODE CONNECTION: JSON parsing is like tree traversal (LC #94, #144) -// We navigate a nested structure to extract values. -// -// Also similar to LC #1 Two Sum - we're doing key lookups in a map. -// ============================================================================= - -/// Parse lsblk JSON output into storage devices. -/// -/// # Arguments -/// -/// * `output` - JSON output from `lsblk -J -o NAME,SIZE,TYPE,MODEL,SERIAL,ROTA,TRAN,WWN -b` -/// -/// # Expected Format -/// -/// ```json -/// { -/// "blockdevices": [ -/// { -/// "name": "nvme0n1", -/// "size": 2000398934016, -/// "type": "disk", -/// "model": "Samsung SSD 980 PRO 2TB", -/// "serial": "S5GXNF0N123456", -/// "rota": false, -/// "tran": "nvme", -/// "wwn": "eui.0025385b21404321" -/// } -/// ] -/// } -/// ``` -/// -/// # Notes -/// -/// - Use `-b` flag to get size in bytes (not human-readable) -/// - `rota` is boolean (false = SSD, true = HDD) -/// - `tran` is transport type (nvme, sata, usb, etc.) -/// -/// # References -/// -/// - [lsblk man page](https://man7.org/linux/man-pages/man8/lsblk.8.html) -pub fn parse_lsblk_json(output: &str) -> Result, String> { - // STEP 1: Parse JSON - // Using serde_json which is already a dependency - let json: serde_json::Value = serde_json::from_str(output) - .map_err(|e| format!("Failed to parse lsblk JSON: {}", e))?; - - // STEP 2: Navigate to blockdevices array - // This is like tree traversal - we're finding a specific node - let devices_array = json - .get("blockdevices") - .and_then(|v| v.as_array()) - .ok_or_else(|| "Missing 'blockdevices' array in lsblk output".to_string())?; - - // STEP 3: Transform each JSON object into StorageDevice - // LEETCODE CONNECTION: This is the "transform" pattern seen in many problems - // Like LC #2 Add Two Numbers - transform input format to output format - let mut devices = Vec::new(); - - for device_json in devices_array { - // Skip non-disk entries (partitions, etc.) - let device_type_str = device_json - .get("type") - .and_then(|v| v.as_str()) - .unwrap_or(""); - - if device_type_str != "disk" { - continue; - } - - // Extract fields with defaults for missing values - let name = device_json - .get("name") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); - - // Skip virtual devices - if is_virtual_device(&name) { - continue; - } - - let size_bytes = device_json - .get("size") - .and_then(|v| v.as_u64()) - .unwrap_or(0); - - let model = device_json - .get("model") - .and_then(|v| v.as_str()) - .unwrap_or("") - .trim() // Models often have trailing whitespace - .to_string(); - - let serial = device_json - .get("serial") - .and_then(|v| v.as_str()) - .map(|s| s.trim().to_string()); - - let is_rotational = device_json - .get("rota") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - let transport = device_json - .get("tran") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); - - let wwn = device_json - .get("wwn") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - - // Determine storage type - let device_type = StorageType::from_device(&name, is_rotational); - - // Determine interface from transport - let interface = match transport.as_str() { - "nvme" => "NVMe".to_string(), - "sata" => "SATA".to_string(), - "sas" => "SAS".to_string(), - "usb" => "USB".to_string(), - "" => device_type.display_name().to_string(), - other => other.to_uppercase(), - }; - - // Build the device struct - let mut device = StorageDevice { - name: name.clone(), - device_path: format!("/dev/{}", name), - device_type, - type_: device_type.display_name().to_string(), - size_bytes, - model, - serial_number: serial, - interface, - is_rotational, - wwn, - detection_method: "lsblk".to_string(), - ..Default::default() - }; - - // Calculate convenience fields - device.calculate_size_fields(); - - devices.push(device); - } - - Ok(devices) -} - -// ============================================================================= -// VIRTUAL DEVICE DETECTION -// ============================================================================= -// -// LEETCODE CONNECTION: This is pattern matching, similar to: -// - LC #10 Regular Expression Matching -// - LC #44 Wildcard Matching -// -// We're checking if a string matches any of several patterns. -// ============================================================================= - -/// Check if device name indicates a virtual device. -/// -/// Virtual devices are not physical hardware and should usually be -/// filtered out of hardware inventory. -/// -/// # Arguments -/// -/// * `name` - Block device name -/// -/// # Returns -/// -/// `true` if device is virtual (loop, ram, dm-*, etc.) -/// -/// # Virtual Device Types -/// -/// | Prefix | Description | -/// |--------|-------------| -/// | loop | Loop devices (mounted ISO files, etc.) | -/// | ram | RAM disks | -/// | dm- | Device mapper (LVM, LUKS encryption) | -/// | zram | Compressed RAM for swap | -/// | nbd | Network block device | -/// -/// # Example -/// -/// ```rust -/// use hardware_report::domain::parsers::storage::is_virtual_device; -/// -/// assert!(is_virtual_device("loop0")); -/// assert!(is_virtual_device("dm-0")); -/// assert!(!is_virtual_device("sda")); -/// assert!(!is_virtual_device("nvme0n1")); -/// ``` -pub fn is_virtual_device(name: &str) -> bool { - // Check prefixes that indicate virtual devices - // Order doesn't matter for correctness, but put common ones first for efficiency - name.starts_with("loop") - || name.starts_with("dm-") - || name.starts_with("ram") - || name.starts_with("zram") - || name.starts_with("nbd") - || name.starts_with("sr") // CD/DVD drives (virtual in VMs) -} - -// ============================================================================= -// HUMAN-READABLE SIZE PARSING -// ============================================================================= -// -// LEETCODE CONNECTION: This is like LC #8 (atoi) but with unit suffixes. -// We need to handle: "500 GB", "2 TB", "1.5 TB", etc. -// -// Pattern: Parse number + parse unit + multiply -// ============================================================================= - -/// Parse human-readable size string to bytes. -/// -/// Handles common size formats from various tools. -/// -/// # Supported Formats -/// -/// - "500 GB", "500GB", "500G" -/// - "2 TB", "2TB", "2T" -/// - "1.5 TB" -/// - "1000000000" (raw bytes) -/// -/// # Units (Binary) -/// -/// - K/KB = 1024 -/// - M/MB = 1024² -/// - G/GB = 1024³ -/// - T/TB = 1024⁴ -/// -/// # Example -/// -/// ```rust -/// use hardware_report::domain::parsers::storage::parse_size_string; -/// -/// assert_eq!(parse_size_string("500 GB"), Some(500 * 1024_u64.pow(3))); -/// assert_eq!(parse_size_string("2 TB"), Some(2 * 1024_u64.pow(4))); -/// assert_eq!(parse_size_string("1.5 TB"), Some((1.5 * 1024_f64.powi(4)) as u64)); -/// ``` -pub fn parse_size_string(size_str: &str) -> Option { - let s = size_str.trim().to_uppercase(); - - // Handle "No Module Installed" or similar - if s.contains("NO ") || s.contains("UNKNOWN") || s.is_empty() { - return None; - } - - // Try to parse as raw number first - if let Ok(bytes) = s.parse::() { - return Some(bytes); - } - - // PATTERN: Split into number and unit - // "500 GB" -> ["500", "GB"] - // "500GB" -> need to find where number ends - - // Find where the number part ends - let num_end = s - .chars() - .position(|c| !c.is_ascii_digit() && c != '.') - .unwrap_or(s.len()); - - if num_end == 0 { - return None; - } - - let num_str = &s[..num_end]; - let unit_str = s[num_end..].trim(); - - // Parse the number (could be float like "1.5") - let num: f64 = num_str.parse().ok()?; - - // Determine multiplier based on unit - const KB: u64 = 1024; - const MB: u64 = KB * 1024; - const GB: u64 = MB * 1024; - const TB: u64 = GB * 1024; - - let multiplier = match unit_str { - "K" | "KB" | "KIB" => KB, - "M" | "MB" | "MIB" => MB, - "G" | "GB" | "GIB" => GB, - "T" | "TB" | "TIB" => TB, - "B" | "" => 1, - _ => return None, - }; - - Some((num * multiplier as f64) as u64) -} - -// ============================================================================= -// UNIT TESTS -// ============================================================================= -// -// IMPORTANT: Always test pure functions! They're easy to test -// because they have no dependencies. -// ============================================================================= - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_sysfs_size() { - // 2TB drive (approximately 3.9 billion sectors) - assert_eq!( - parse_sysfs_size("3907029168").unwrap(), - 3907029168 * 512 - ); - - // With whitespace - assert_eq!( - parse_sysfs_size(" 1000000\n").unwrap(), - 1000000 * 512 - ); - - // Error case - assert!(parse_sysfs_size("not a number").is_err()); - assert!(parse_sysfs_size("").is_err()); - } - - #[test] - fn test_parse_sysfs_rotational() { - assert!(parse_sysfs_rotational("1")); - assert!(!parse_sysfs_rotational("0")); - assert!(!parse_sysfs_rotational("0\n")); - assert!(!parse_sysfs_rotational("")); - assert!(!parse_sysfs_rotational("garbage")); - } - - #[test] - fn test_is_virtual_device() { - // Virtual devices - assert!(is_virtual_device("loop0")); - assert!(is_virtual_device("loop1")); - assert!(is_virtual_device("dm-0")); - assert!(is_virtual_device("dm-1")); - assert!(is_virtual_device("ram0")); - assert!(is_virtual_device("zram0")); - assert!(is_virtual_device("nbd0")); - - // Physical devices - assert!(!is_virtual_device("sda")); - assert!(!is_virtual_device("sdb")); - assert!(!is_virtual_device("nvme0n1")); - assert!(!is_virtual_device("mmcblk0")); - } - - #[test] - fn test_storage_type_from_device() { - // NVMe - assert_eq!(StorageType::from_device("nvme0n1", false), StorageType::Nvme); - assert_eq!(StorageType::from_device("nvme1n1", false), StorageType::Nvme); - - // eMMC - assert_eq!(StorageType::from_device("mmcblk0", false), StorageType::Emmc); - - // Virtual - assert_eq!(StorageType::from_device("loop0", false), StorageType::Virtual); - assert_eq!(StorageType::from_device("dm-0", false), StorageType::Virtual); - - // SSD vs HDD (based on rotational flag) - assert_eq!(StorageType::from_device("sda", false), StorageType::Ssd); - assert_eq!(StorageType::from_device("sda", true), StorageType::Hdd); - } - - #[test] - fn test_parse_size_string() { - // GB - assert_eq!(parse_size_string("500 GB"), Some(500 * 1024_u64.pow(3))); - assert_eq!(parse_size_string("500GB"), Some(500 * 1024_u64.pow(3))); - - // TB - assert_eq!(parse_size_string("2 TB"), Some(2 * 1024_u64.pow(4))); - - // Raw bytes - assert_eq!(parse_size_string("1073741824"), Some(1073741824)); - - // Invalid - assert_eq!(parse_size_string("Unknown"), None); - assert_eq!(parse_size_string(""), None); - } -} -``` - -### 1.4 Update Linux Adapter for Storage - -**File:** `src/adapters/secondary/system/linux.rs` - -**Where:** Replace/update the `get_storage_info` method - -**LeetCode Pattern:** This implements **Chain of Responsibility** - we try multiple -detection methods in sequence until one succeeds. Similar to how you might try -multiple algorithms for optimization. - -```rust -// ============================================================================= -// STORAGE DETECTION IN LINUX ADAPTER -// ============================================================================= -// -// ARCHITECTURE: This is the ADAPTER layer implementation of SystemInfoProvider. -// It implements the PORT (trait) using Linux-specific mechanisms. -// -// PATTERN: Chain of Responsibility -// - Try sysfs first (most reliable) -// - Fall back to lsblk if sysfs fails -// - Use sysinfo as last resort -// -// LEETCODE CONNECTION: This pattern is used when you have multiple approaches: -// - LC #70 Climbing Stairs: try 1 step, try 2 steps -// - LC #322 Coin Change: try each coin denomination -// - Here: try each detection method -// ============================================================================= - -// Add these imports at the top of linux.rs -use crate::domain::parsers::storage::{ - parse_sysfs_size, parse_sysfs_rotational, parse_lsblk_json, is_virtual_device -}; -use crate::domain::{StorageDevice, StorageInfo, StorageType}; -use std::fs; -use std::path::Path; - -impl SystemInfoProvider for LinuxSystemInfoProvider { - // ... other methods ... - - /// Detect storage devices using multiple methods. - /// - /// # Detection Chain - /// - /// ```text - /// ┌─────────────────────────────────────────────────────────┐ - /// │ 1. sysfs /sys/block (PRIMARY) │ - /// │ - Most reliable │ - /// │ - Works on all Linux (x86, ARM) │ - /// │ - Direct kernel interface │ - /// └───────────────────────┬─────────────────────────────────┘ - /// │ enrich with - /// ▼ - /// ┌─────────────────────────────────────────────────────────┐ - /// │ 2. lsblk JSON (ENRICHMENT) │ - /// │ - Additional fields (WWN, transport) │ - /// │ - Serial number (may be available) │ - /// └───────────────────────┬─────────────────────────────────┘ - /// │ if empty, fallback - /// ▼ - /// ┌─────────────────────────────────────────────────────────┐ - /// │ 3. sysinfo crate (FALLBACK) │ - /// │ - Cross-platform │ - /// │ - Limited metadata │ - /// └─────────────────────────────────────────────────────────┘ - /// ``` - async fn get_storage_info(&self) -> Result { - let mut devices = Vec::new(); - - // ===================================================================== - // METHOD 1: sysfs (Primary - most reliable) - // ===================================================================== - // - // WHY SYSFS FIRST? - // - Direct kernel interface, always available on Linux - // - Doesn't require external tools (lsblk might not be installed) - // - Works identically on x86 and ARM - // ===================================================================== - - match self.detect_storage_sysfs().await { - Ok(sysfs_devices) => { - log::debug!("sysfs detected {} storage devices", sysfs_devices.len()); - devices = sysfs_devices; - } - Err(e) => { - log::warn!("sysfs storage detection failed: {}", e); - } - } - - // ===================================================================== - // METHOD 2: lsblk enrichment - // ===================================================================== - // - // Even if sysfs worked, lsblk might have additional data (WWN, etc.) - // We MERGE the results rather than replace. - // - // LEETCODE CONNECTION: Merging data is like: - // - LC #88 Merge Sorted Array - // - LC #21 Merge Two Sorted Lists - // Key insight: match by device name, then combine fields - // ===================================================================== - - if let Ok(lsblk_devices) = self.detect_storage_lsblk().await { - log::debug!("lsblk detected {} devices for enrichment", lsblk_devices.len()); - merge_storage_info(&mut devices, lsblk_devices); - } - - // ===================================================================== - // METHOD 3: sysinfo fallback - // ===================================================================== - // - // If we still have no devices, try sysinfo as last resort. - // This can happen in containers or unusual environments. - // ===================================================================== - - if devices.is_empty() { - log::warn!("No devices from sysfs/lsblk, trying sysinfo fallback"); - if let Ok(sysinfo_devices) = self.detect_storage_sysinfo().await { - devices = sysinfo_devices; - } - } - - // ===================================================================== - // POST-PROCESSING - // ===================================================================== - - // Filter out virtual devices (they're not physical hardware) - devices.retain(|d| d.device_type != StorageType::Virtual); - - // Ensure all devices have calculated size fields - for device in &mut devices { - if device.size_gb == 0.0 && device.size_bytes > 0 { - device.calculate_size_fields(); - } - device.set_device_path(); - } - - // Sort by name for consistent output - devices.sort_by(|a, b| a.name.cmp(&b.name)); - - Ok(StorageInfo { devices }) - } -} - -// ============================================================================= -// HELPER METHODS FOR STORAGE DETECTION -// ============================================================================= - -impl LinuxSystemInfoProvider { - /// Detect storage devices via sysfs. - /// - /// Reads directly from `/sys/block` which is the kernel's view of - /// block devices. - /// - /// # sysfs Structure - /// - /// ```text - /// /sys/block/{device}/ - /// ├── size # Size in 512-byte sectors - /// ├── queue/ - /// │ └── rotational # 0=SSD, 1=HDD - /// └── device/ - /// ├── model # Device model name - /// ├── serial # Serial number (may need root) - /// └── firmware_rev # Firmware version - /// ``` - /// - /// # Returns - /// - /// Vector of storage devices found in sysfs. - async fn detect_storage_sysfs(&self) -> Result, SystemError> { - let mut devices = Vec::new(); - - // Path to block devices in sysfs - let sys_block = Path::new("/sys/block"); - - if !sys_block.exists() { - return Err(SystemError::NotAvailable { - resource: "/sys/block".to_string(), - }); - } - - // LEETCODE CONNECTION: This is directory traversal, similar to: - // - LC #200 Number of Islands (grid traversal) - // - LC #130 Surrounded Regions - // We're walking a filesystem tree - - let entries = fs::read_dir(sys_block).map_err(|e| SystemError::IoError { - path: "/sys/block".to_string(), - message: e.to_string(), - })?; - - for entry in entries.flatten() { - let device_name = entry.file_name().to_string_lossy().to_string(); - - // Skip virtual devices early (no need to read their attributes) - if is_virtual_device(&device_name) { - continue; - } - - let device_path = entry.path(); - - // Read size (required - skip device if we can't get size) - let size_path = device_path.join("size"); - let size_bytes = match fs::read_to_string(&size_path) { - Ok(content) => match parse_sysfs_size(&content) { - Ok(size) => size, - Err(_) => continue, // Skip devices we can't parse - }, - Err(_) => continue, - }; - - // Skip tiny devices (< 1GB, probably not real storage) - if size_bytes < 1_000_000_000 { - continue; - } - - // Read rotational flag - let rotational_path = device_path.join("queue/rotational"); - let is_rotational = fs::read_to_string(&rotational_path) - .map(|content| parse_sysfs_rotational(&content)) - .unwrap_or(false); - - // Determine device type - let device_type = StorageType::from_device(&device_name, is_rotational); - - // Read model (in device subdirectory) - let model = self.read_sysfs_string(&device_path.join("device/model")) - .unwrap_or_default() - .trim() - .to_string(); - - // Read serial (may require root) - let serial_number = self.read_sysfs_string(&device_path.join("device/serial")) - .map(|s| s.trim().to_string()) - .ok(); - - // Read firmware version - let firmware_version = self.read_sysfs_string(&device_path.join("device/firmware_rev")) - .map(|s| s.trim().to_string()) - .ok(); - - // Determine interface based on device type - let interface = match &device_type { - StorageType::Nvme => "NVMe".to_string(), - StorageType::Emmc => "eMMC".to_string(), - StorageType::Hdd | StorageType::Ssd => { - // Could check for SAS vs SATA here - "SATA".to_string() - } - _ => "Unknown".to_string(), - }; - - // Build the device - let mut device = StorageDevice { - name: device_name.clone(), - device_path: format!("/dev/{}", device_name), - device_type: device_type.clone(), - type_: device_type.display_name().to_string(), - size_bytes, - model, - serial_number, - firmware_version, - interface, - is_rotational, - detection_method: "sysfs".to_string(), - ..Default::default() - }; - - device.calculate_size_fields(); - devices.push(device); - } - - Ok(devices) - } - - /// Detect storage via lsblk command. - /// - /// Uses JSON output for reliable parsing. - async fn detect_storage_lsblk(&self) -> Result, SystemError> { - let cmd = SystemCommand::new("lsblk") - .args(&[ - "-J", // JSON output - "-b", // Size in bytes - "-o", "NAME,SIZE,TYPE,MODEL,SERIAL,ROTA,TRAN,WWN", - ]) - .timeout(Duration::from_secs(10)); - - let output = self.command_executor.execute(&cmd).await.map_err(|e| { - SystemError::CommandFailed { - command: "lsblk".to_string(), - exit_code: None, - stderr: e.to_string(), - } - })?; - - if !output.success { - return Err(SystemError::CommandFailed { - command: "lsblk".to_string(), - exit_code: output.exit_code, - stderr: output.stderr.clone(), - }); - } - - parse_lsblk_json(&output.stdout).map_err(SystemError::ParseError) - } - - /// Detect storage via sysinfo crate (cross-platform fallback). - async fn detect_storage_sysinfo(&self) -> Result, SystemError> { - use sysinfo::Disks; - - let disks = Disks::new_with_refreshed_list(); - let mut devices = Vec::new(); - - for disk in disks.iter() { - let name = disk.name().to_string_lossy().to_string(); - let size_bytes = disk.total_space(); - - // Skip small/virtual - if size_bytes < 1_000_000_000 { - continue; - } - - let mut device = StorageDevice { - name: if name.is_empty() { - disk.mount_point().to_string_lossy().to_string() - } else { - name - }, - size_bytes, - detection_method: "sysinfo".to_string(), - ..Default::default() - }; - - device.calculate_size_fields(); - devices.push(device); - } - - Ok(devices) - } - - /// Helper to read a sysfs file as string. - fn read_sysfs_string(&self, path: &Path) -> Result { - fs::read_to_string(path) - } -} - -// ============================================================================= -// MERGE FUNCTION -// ============================================================================= -// -// LEETCODE CONNECTION: This is the merge pattern from: -// - LC #88 Merge Sorted Array -// - LC #56 Merge Intervals -// -// Key insight: We match by device name, then update fields that are missing -// in the primary source but present in the secondary. -// ============================================================================= - -/// Merge storage info from secondary source into primary. -/// -/// Matches devices by name and fills in missing fields. -/// -/// # Why Merge? -/// -/// Different detection methods provide different data: -/// - sysfs: reliable size, rotational flag -/// - lsblk: WWN, transport type -/// - smartctl: serial, SMART status -/// -/// By merging, we get the best of all sources. -fn merge_storage_info(primary: &mut Vec, secondary: Vec) { - // LEETCODE CONNECTION: This is O(n*m) where n = primary.len(), m = secondary.len() - // Could optimize with HashMap for O(n+m) if lists are large - // - // For small lists (typically < 20 devices), linear search is fine - - for sec_device in secondary { - // Find matching device in primary by name - if let Some(pri_device) = primary.iter_mut().find(|d| d.name == sec_device.name) { - // Fill in missing fields from secondary - // Only update if primary field is empty/None - - if pri_device.serial_number.is_none() { - pri_device.serial_number = sec_device.serial_number; - } - - if pri_device.firmware_version.is_none() { - pri_device.firmware_version = sec_device.firmware_version; - } - - if pri_device.wwn.is_none() { - pri_device.wwn = sec_device.wwn; - } - - if pri_device.model.is_empty() && !sec_device.model.is_empty() { - pri_device.model = sec_device.model; - } - } else { - // Device not in primary - add it - // This handles cases where sysfs missed a device but lsblk found it - primary.push(sec_device); - } - } -} -``` - ---- - -## Step 2: CPU Enhancements - -### 2.1 Update CpuInfo Struct - -**File:** `src/domain/entities.rs` - -**Where:** Replace the existing `CpuInfo` struct (around line 163-175) - -**LeetCode Pattern:** The cache hierarchy (L1/L2/L3) is a tree structure. Understanding -cache levels is similar to tree level traversal (LC #102, #107). - -```rust -// ============================================================================= -// CPU CACHE INFO STRUCT -// ============================================================================= -// -// WHY: CPU caches are hierarchical (L1 → L2 → L3), each with different -// characteristics. Understanding this is like understanding tree levels. -// -// LEETCODE CONNECTION: Cache hierarchy is like tree levels: -// - LC #102 Binary Tree Level Order Traversal -// - LC #107 Binary Tree Level Order Traversal II -// - L1 = leaf level (fastest, smallest) -// - L3 = root level (slowest, largest) -// ============================================================================= - -/// CPU cache level information. -/// -/// Represents a single cache level (L1d, L1i, L2, L3). -/// Each core has its own L1/L2, while L3 is typically shared. -/// -/// # Cache Hierarchy -/// -/// ```text -/// ┌─────────────────────┐ -/// │ L3 Cache │ ← Shared across cores -/// │ (8-256 MB) │ Slowest but largest -/// └──────────┬──────────┘ -/// │ -/// ┌───────────────────┼───────────────────┐ -/// │ │ │ -/// ┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐ -/// │ L2 Cache │ │ L2 Cache │ │ L2 Cache │ -/// │ (256KB-1MB) │ │ (per core) │ │ │ -/// └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ -/// │ │ │ -/// ┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐ -/// │ L1d │ L1i │ │ L1d │ L1i │ │ L1d │ L1i │ -/// │(32KB each) │ │ (per core) │ │ │ -/// └─────────────┘ └─────────────┘ └─────────────┘ -/// Core 0 Core 1 Core N -/// ``` -/// -/// # References -/// -/// - [CPU Cache Wikipedia](https://en.wikipedia.org/wiki/CPU_cache) -/// - [Linux cache sysfs](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-system-cpu) -#[derive(Debug, Serialize, Deserialize, Clone, Default)] -pub struct CpuCacheInfo { - /// Cache level (1, 2, or 3). - pub level: u8, - - /// Cache type. - /// - /// Values: "Data" (L1d), "Instruction" (L1i), "Unified" (L2, L3) - pub cache_type: String, - - /// Cache size in kilobytes. - pub size_kb: u32, - - /// Number of ways of associativity. - /// - /// Higher = more flexible but complex. - /// Common values: 4, 8, 12, 16 - #[serde(skip_serializing_if = "Option::is_none")] - pub ways_of_associativity: Option, - - /// Cache line size in bytes. - /// - /// Typically 64 bytes on modern CPUs. - /// Important for avoiding false sharing. - #[serde(skip_serializing_if = "Option::is_none")] - pub line_size_bytes: Option, - - /// Number of sets. - #[serde(skip_serializing_if = "Option::is_none")] - pub sets: Option, - - /// Whether this cache is shared across cores. - #[serde(skip_serializing_if = "Option::is_none")] - pub shared: Option, -} - -// ============================================================================= -// CPU INFO STRUCT -// ============================================================================= -// -// WHY: The old struct had: -// - speed: String → Can't sort/compare CPUs by frequency -// - No cache info → Missing important performance data -// - No architecture → Can't distinguish x86 from ARM -// -// LEETCODE CONNECTION: CPU topology is a tree: -// - System → Sockets → Cores → Threads -// - Similar to LC #429 N-ary Tree Level Order Traversal -// ============================================================================= - -/// CPU information with extended details. -/// -/// Provides comprehensive CPU information including frequency, -/// cache hierarchy, and feature flags. -/// -/// # Detection Methods -/// -/// Information is gathered from multiple sources (Chain of Responsibility): -/// 1. **sysfs** `/sys/devices/system/cpu` - Frequency, cache -/// 2. **raw-cpuid** - CPUID instruction (x86 only) -/// 3. **/proc/cpuinfo** - Model, vendor, flags -/// 4. **lscpu** - Topology -/// 5. **dmidecode** - SMBIOS data -/// 6. **sysinfo** - Cross-platform fallback -/// -/// # Topology -/// -/// ```text -/// System -/// └── Socket 0 (physical CPU package) -/// ├── Core 0 -/// │ ├── Thread 0 (logical CPU 0) -/// │ └── Thread 1 (logical CPU 1, if SMT/HT enabled) -/// └── Core 1 -/// ├── Thread 0 (logical CPU 2) -/// └── Thread 1 (logical CPU 3) -/// └── Socket 1 (if multi-socket) -/// └── ... -/// ``` -/// -/// # Example -/// -/// ```rust -/// use hardware_report::CpuInfo; -/// -/// // Check if CPU has AVX-512 for vectorized workloads -/// let has_avx512 = cpu.flags.iter().any(|f| f.starts_with("avx512")); -/// -/// // Calculate total compute units -/// let total_threads = cpu.sockets * cpu.cores * cpu.threads; -/// ``` -/// -/// # References -/// -/// - [Linux CPU sysfs](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-system-cpu) -/// - [Intel CPUID](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) -/// - [ARM CPU ID](https://developer.arm.com/documentation/ddi0487/latest) -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct CpuInfo { - // ========================================================================= - // IDENTIFICATION - // ========================================================================= - - /// CPU model name. - /// - /// Examples: - /// - "AMD EPYC 7763 64-Core Processor" - /// - "Intel(R) Xeon(R) Platinum 8380 CPU @ 2.30GHz" - /// - "Neoverse-N1" (ARM) - pub model: String, - - /// CPU vendor identifier. - /// - /// Values: - /// - "GenuineIntel" (Intel) - /// - "AuthenticAMD" (AMD) - /// - "ARM" (ARM-based) - #[serde(default)] - pub vendor: String, - - // ========================================================================= - // TOPOLOGY - // ========================================================================= - // - // LEETCODE CONNECTION: Understanding topology is like tree traversal - // total_threads = sockets × cores × threads_per_core - // ========================================================================= - - /// Physical cores per socket. - pub cores: u32, - - /// Threads per core (SMT/Hyperthreading). - /// - /// Usually 1 (no SMT) or 2 (SMT enabled). - pub threads: u32, - - /// Number of CPU sockets. - /// - /// Desktop: 1, Server: 1-8 - pub sockets: u32, - - /// Total physical cores (cores × sockets). - #[serde(default)] - pub total_cores: u32, - - /// Total logical CPUs (cores × threads × sockets). - #[serde(default)] - pub total_threads: u32, - - // ========================================================================= - // FREQUENCY - // ========================================================================= - // - // WHY MULTIPLE FREQUENCIES? - // - base = guaranteed frequency - // - max = turbo/boost frequency (brief bursts) - // - min = power-saving frequency - // ========================================================================= - - /// CPU frequency in MHz. - /// - /// This is the PRIMARY frequency field (current or max). - /// Use for CMDB inventory and general reporting. - #[serde(default)] - pub frequency_mhz: u32, - - /// Legacy speed field as string. - /// - /// DEPRECATED: Use `frequency_mhz` instead. - pub speed: String, - - /// Minimum scaling frequency in MHz. - /// - /// From cpufreq scaling_min_freq (power saving). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub frequency_min_mhz: Option, - - /// Maximum scaling frequency in MHz. - /// - /// From cpufreq scaling_max_freq (turbo/boost). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub frequency_max_mhz: Option, - - /// Base (non-turbo) frequency in MHz. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub frequency_base_mhz: Option, - - // ========================================================================= - // ARCHITECTURE - // ========================================================================= - - /// CPU architecture. - /// - /// Values: "x86_64", "aarch64", "armv7l" - #[serde(default)] - pub architecture: String, - - /// CPU microarchitecture name. - /// - /// Examples: - /// - Intel: "Ice Lake", "Sapphire Rapids" - /// - AMD: "Zen3", "Zen4" - /// - ARM: "Neoverse N1", "Neoverse V2" - #[serde(default, skip_serializing_if = "Option::is_none")] - pub microarchitecture: Option, - - // ========================================================================= - // CACHE SIZES - // ========================================================================= - // - // WHY SEPARATE L1d AND L1i? - // - L1d = data cache (for variables, arrays) - // - L1i = instruction cache (for code) - // - They're accessed differently, may have different sizes - // ========================================================================= - - /// L1 data cache size in KB (per core). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cache_l1d_kb: Option, - - /// L1 instruction cache size in KB (per core). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cache_l1i_kb: Option, - - /// L2 cache size in KB (usually per core). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cache_l2_kb: Option, - - /// L3 cache size in KB (usually shared). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cache_l3_kb: Option, - - /// Detailed cache information. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub caches: Vec, - - // ========================================================================= - // FEATURES AND FLAGS - // ========================================================================= - // - // LEETCODE CONNECTION: Checking flags is like set membership (LC #217) - // "Does this CPU support AVX-512?" = "Is avx512f in the set?" - // ========================================================================= - - /// CPU feature flags. - /// - /// x86 examples: "avx", "avx2", "avx512f", "aes", "sse4_2" - /// ARM examples: "fp", "asimd", "sve", "sve2" - /// - /// # Usage - /// - /// ```rust - /// // Check for AVX-512 support - /// let has_avx512 = cpu.flags.iter().any(|f| f.starts_with("avx512")); - /// - /// // Check for AES-NI (hardware encryption) - /// let has_aes = cpu.flags.contains(&"aes".to_string()); - /// ``` - #[serde(default)] - pub flags: Vec, - - // ========================================================================= - // ADDITIONAL METADATA - // ========================================================================= - - /// Microcode/firmware version. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub microcode_version: Option, - - /// CPU stepping (silicon revision). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub stepping: Option, - - /// CPU family number. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub family: Option, - - /// CPU model number (not the name). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub model_number: Option, - - /// Virtualization technology. - /// - /// Values: "VT-x" (Intel), "AMD-V" (AMD), "none" - #[serde(default, skip_serializing_if = "Option::is_none")] - pub virtualization: Option, - - /// Number of NUMA nodes. - #[serde(default)] - pub numa_nodes: u32, - - /// Detection methods used. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub detection_methods: Vec, -} - -impl Default for CpuInfo { - fn default() -> Self { - Self { - model: String::new(), - vendor: String::new(), - cores: 0, - threads: 1, - sockets: 1, - total_cores: 0, - total_threads: 0, - frequency_mhz: 0, - speed: String::new(), - frequency_min_mhz: None, - frequency_max_mhz: None, - frequency_base_mhz: None, - architecture: std::env::consts::ARCH.to_string(), - microarchitecture: None, - cache_l1d_kb: None, - cache_l1i_kb: None, - cache_l2_kb: None, - cache_l3_kb: None, - caches: Vec::new(), - flags: Vec::new(), - microcode_version: None, - stepping: None, - family: None, - model_number: None, - virtualization: None, - numa_nodes: 1, - detection_methods: Vec::new(), - } - } -} - -impl CpuInfo { - /// Calculate total_cores and total_threads from topology. - pub fn calculate_totals(&mut self) { - self.total_cores = self.sockets * self.cores; - self.total_threads = self.total_cores * self.threads; - } - - /// Set legacy speed field from frequency_mhz. - pub fn set_speed_string(&mut self) { - if self.frequency_mhz > 0 { - if self.frequency_mhz >= 1000 { - self.speed = format!("{:.2} GHz", self.frequency_mhz as f64 / 1000.0); - } else { - self.speed = format!("{} MHz", self.frequency_mhz); - } - } - } -} -``` - -### 2.2 Add CPU Parser Functions - -**File:** `src/domain/parsers/cpu.rs` - -**LeetCode Pattern:** Parsing /proc/cpuinfo is a **State Machine** problem similar to -LC #65 (Valid Number) - we track state while processing each line. - -```rust -// ============================================================================= -// CPU PARSER FUNCTIONS -// ============================================================================= -// -// Architecture: DOMAIN layer - pure functions, no I/O -// ============================================================================= - -use crate::domain::{CpuInfo, CpuCacheInfo}; - -// ============================================================================= -// SYSFS FREQUENCY PARSING -// ============================================================================= -// -// LEETCODE CONNECTION: This is number parsing like LC #8 (atoi) -// but with unit conversion (kHz to MHz). -// ============================================================================= - -/// Parse sysfs frequency file (kHz) to MHz. -/// -/// The kernel reports CPU frequencies in kHz in sysfs. -/// -/// # Arguments -/// -/// * `content` - Content of cpufreq file (in kHz) -/// -/// # Example -/// -/// ```rust -/// use hardware_report::domain::parsers::cpu::parse_sysfs_freq_khz; -/// -/// // 3.5 GHz in kHz -/// assert_eq!(parse_sysfs_freq_khz("3500000").unwrap(), 3500); -/// assert_eq!(parse_sysfs_freq_khz("2100000\n").unwrap(), 2100); -/// ``` -pub fn parse_sysfs_freq_khz(content: &str) -> Result { - let khz: u32 = content - .trim() - .parse() - .map_err(|e| format!("Invalid frequency '{}': {}", content.trim(), e))?; - - // Convert kHz to MHz - Ok(khz / 1000) -} - -// ============================================================================= -// SYSFS CACHE SIZE PARSING -// ============================================================================= -// -// LEETCODE CONNECTION: Similar to LC #8 but with unit suffixes (K, M, G) -// Need to handle: "32K", "1M", "256K", "16M" -// ============================================================================= - -/// Parse sysfs cache size (e.g., "32K", "1M") to KB. -/// -/// # Arguments -/// -/// * `content` - Content of cache size file -/// -/// # Supported Units -/// -/// - K = kilobytes (multiply by 1) -/// - M = megabytes (multiply by 1024) -/// - G = gigabytes (multiply by 1024²) -/// -/// # Example -/// -/// ```rust -/// use hardware_report::domain::parsers::cpu::parse_sysfs_cache_size; -/// -/// assert_eq!(parse_sysfs_cache_size("32K").unwrap(), 32); -/// assert_eq!(parse_sysfs_cache_size("1M").unwrap(), 1024); -/// assert_eq!(parse_sysfs_cache_size("256K").unwrap(), 256); -/// ``` -pub fn parse_sysfs_cache_size(content: &str) -> Result { - let s = content.trim().to_uppercase(); - - // Handle common formats: "32K", "1M", "32768K" - if s.ends_with('K') { - let num_str = &s[..s.len()-1]; - num_str.parse::() - .map_err(|e| format!("Invalid cache size '{}': {}", s, e)) - } else if s.ends_with('M') { - let num_str = &s[..s.len()-1]; - num_str.parse::() - .map(|v| v * 1024) - .map_err(|e| format!("Invalid cache size '{}': {}", s, e)) - } else if s.ends_with('G') { - let num_str = &s[..s.len()-1]; - num_str.parse::() - .map(|v| v * 1024 * 1024) - .map_err(|e| format!("Invalid cache size '{}': {}", s, e)) - } else { - // Assume raw KB value - s.parse::() - .map_err(|e| format!("Invalid cache size '{}': {}", s, e)) - } -} - -// ============================================================================= -// /proc/cpuinfo PARSING -// ============================================================================= -// -// LEETCODE CONNECTION: This is a STATE MACHINE problem like: -// - LC #65 Valid Number -// - LC #10 Regular Expression Matching -// -// We process line by line, extracting key:value pairs. -// Different architectures (x86 vs ARM) have different keys! -// ============================================================================= - -/// Parse /proc/cpuinfo into CpuInfo. -/// -/// # Format Differences -/// -/// **x86/x86_64:** -/// ```text -/// model name : Intel(R) Xeon(R) Platinum 8380 CPU @ 2.30GHz -/// vendor_id : GenuineIntel -/// flags : fpu vme de pse avx avx2 avx512f ... -/// ``` -/// -/// **ARM/aarch64:** -/// ```text -/// CPU implementer : 0x41 -/// CPU part : 0xd0c -/// Features : fp asimd evtstrm aes ... -/// ``` -/// -/// # LeetCode Pattern -/// -/// This is similar to parsing problems: -/// - Split by delimiter (`:`) -/// - Handle whitespace -/// - Accumulate results -/// -/// # Example -/// -/// ```rust -/// use hardware_report::domain::parsers::cpu::parse_proc_cpuinfo; -/// -/// let cpuinfo = "model name\t: Intel Xeon\nflags\t: avx avx2\n"; -/// let info = parse_proc_cpuinfo(cpuinfo).unwrap(); -/// assert!(info.flags.contains(&"avx".to_string())); -/// ``` -pub fn parse_proc_cpuinfo(content: &str) -> Result { - let mut info = CpuInfo::default(); - let mut processor_count = 0; - - // Process each line - // PATTERN: Key-value parsing with colon delimiter - for line in content.lines() { - // Split on first colon - // "model name\t: Intel Xeon" -> ["model name\t", " Intel Xeon"] - let parts: Vec<&str> = line.splitn(2, ':').collect(); - - if parts.len() != 2 { - continue; - } - - let key = parts[0].trim().to_lowercase(); - let value = parts[1].trim(); - - // Match on key (different for x86 vs ARM) - match key.as_str() { - // x86 keys - "model name" => { - if info.model.is_empty() { - info.model = value.to_string(); - } - } - "vendor_id" => { - if info.vendor.is_empty() { - info.vendor = value.to_string(); - } - } - "cpu family" => { - info.family = value.parse().ok(); - } - "model" => { - // Note: "model" is the number, "model name" is the string - info.model_number = value.parse().ok(); - } - "stepping" => { - info.stepping = value.parse().ok(); - } - "microcode" => { - info.microcode_version = Some(value.to_string()); - } - "cpu mhz" => { - // Parse frequency from cpuinfo (may be floating point) - if let Ok(mhz) = value.parse::() { - info.frequency_mhz = mhz as u32; - } - } - "flags" => { - // x86 feature flags (space-separated) - info.flags = value.split_whitespace() - .map(String::from) - .collect(); - } - - // ARM keys - "features" => { - // ARM feature flags (like x86 "flags") - info.flags = value.split_whitespace() - .map(String::from) - .collect(); - } - "cpu implementer" => { - // ARM: indicates vendor - if info.vendor.is_empty() { - info.vendor = "ARM".to_string(); - } - } - "cpu part" => { - // ARM: CPU part number -> map to microarchitecture - if let Some(arch_name) = arm_cpu_part_to_name(value) { - info.microarchitecture = Some(arch_name.to_string()); - } - } - - // Count processors - "processor" => { - processor_count += 1; - } - - _ => {} - } - } - - // Set total_threads from processor count - if processor_count > 0 { - info.total_threads = processor_count; - } - - info.detection_methods.push("proc_cpuinfo".to_string()); - - Ok(info) -} - -// ============================================================================= -// ARM CPU PART MAPPING -// ============================================================================= -// -// LEETCODE CONNECTION: This is a HASH MAP lookup problem like: -// - LC #1 Two Sum (lookup in map) -// - LC #49 Group Anagrams (categorization) -// -// ARM CPUs are identified by a part number. We map to human-readable names. -// ============================================================================= - -/// Map ARM CPU part ID to microarchitecture name. -/// -/// ARM CPUs report a "CPU part" number in /proc/cpuinfo. -/// This function maps it to a human-readable name. -/// -/// # Arguments -/// -/// * `part` - CPU part from /proc/cpuinfo (e.g., "0xd0c") -/// -/// # Returns -/// -/// Human-readable microarchitecture name, or None if unknown. -/// -/// # Example -/// -/// ```rust -/// use hardware_report::domain::parsers::cpu::arm_cpu_part_to_name; -/// -/// assert_eq!(arm_cpu_part_to_name("0xd0c"), Some("Neoverse N1")); -/// assert_eq!(arm_cpu_part_to_name("0xd49"), Some("Neoverse N2")); -/// assert_eq!(arm_cpu_part_to_name("0xffff"), None); -/// ``` -/// -/// # References -/// -/// - [ARM CPU Part Numbers](https://developer.arm.com/documentation/ddi0487/latest) -/// - [Kernel ARM CPU table](https://github.com/torvalds/linux/blob/master/arch/arm64/kernel/cpuinfo.c) -pub fn arm_cpu_part_to_name(part: &str) -> Option<&'static str> { - // Normalize: remove "0x" prefix, convert to lowercase - let normalized = part.trim().to_lowercase(); - let part_id = normalized.strip_prefix("0x").unwrap_or(&normalized); - - // LEETCODE CONNECTION: This is essentially a hash map lookup - // In LeetCode terms: O(1) lookup after building the map - // We use match here for compile-time optimization - - match part_id { - // ARM Cortex-A series (mobile/embedded) - "d03" => Some("Cortex-A53"), - "d04" => Some("Cortex-A35"), - "d05" => Some("Cortex-A55"), - "d06" => Some("Cortex-A65"), - "d07" => Some("Cortex-A57"), - "d08" => Some("Cortex-A72"), - "d09" => Some("Cortex-A73"), - "d0a" => Some("Cortex-A75"), - "d0b" => Some("Cortex-A76"), - "d0c" => Some("Neoverse N1"), // Server (AWS Graviton2) - "d0d" => Some("Cortex-A77"), - "d0e" => Some("Cortex-A76AE"), - - // ARM Neoverse (server/cloud) - "d40" => Some("Neoverse V1"), // Server - "d41" => Some("Cortex-A78"), - "d42" => Some("Cortex-A78AE"), - "d43" => Some("Cortex-A65AE"), - "d44" => Some("Cortex-X1"), - "d46" => Some("Cortex-A510"), - "d47" => Some("Cortex-A710"), - "d48" => Some("Cortex-X2"), - "d49" => Some("Neoverse N2"), // Server (AWS Graviton3) - "d4a" => Some("Neoverse E1"), - "d4b" => Some("Cortex-A78C"), - "d4c" => Some("Cortex-X1C"), - "d4d" => Some("Cortex-A715"), - "d4e" => Some("Cortex-X3"), - "d4f" => Some("Neoverse V2"), // Server - - // Newer cores - "d80" => Some("Cortex-A520"), - "d81" => Some("Cortex-A720"), - "d82" => Some("Cortex-X4"), - - // NVIDIA (based on ARM) - "004" => Some("NVIDIA Denver"), - "003" => Some("NVIDIA Carmel"), - - _ => None, - } -} - -// ============================================================================= -// UNIT TESTS -// ============================================================================= - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_sysfs_freq_khz() { - assert_eq!(parse_sysfs_freq_khz("3500000").unwrap(), 3500); - assert_eq!(parse_sysfs_freq_khz("2100000\n").unwrap(), 2100); - assert_eq!(parse_sysfs_freq_khz(" 1000000 ").unwrap(), 1000); - assert!(parse_sysfs_freq_khz("invalid").is_err()); - } - - #[test] - fn test_parse_sysfs_cache_size() { - assert_eq!(parse_sysfs_cache_size("32K").unwrap(), 32); - assert_eq!(parse_sysfs_cache_size("512K").unwrap(), 512); - assert_eq!(parse_sysfs_cache_size("1M").unwrap(), 1024); - assert_eq!(parse_sysfs_cache_size("32M").unwrap(), 32768); - assert_eq!(parse_sysfs_cache_size("32768K").unwrap(), 32768); - } - - #[test] - fn test_arm_cpu_part_mapping() { - assert_eq!(arm_cpu_part_to_name("0xd0c"), Some("Neoverse N1")); - assert_eq!(arm_cpu_part_to_name("0xd49"), Some("Neoverse N2")); - assert_eq!(arm_cpu_part_to_name("d0c"), Some("Neoverse N1")); // Without 0x - assert_eq!(arm_cpu_part_to_name("0xD0C"), Some("Neoverse N1")); // Uppercase - assert_eq!(arm_cpu_part_to_name("0xffff"), None); - } - - #[test] - fn test_parse_proc_cpuinfo_x86() { - let content = r#" -processor : 0 -vendor_id : GenuineIntel -cpu family : 6 -model : 106 -model name : Intel(R) Xeon(R) Platinum 8380 CPU @ 2.30GHz -stepping : 6 -microcode : 0xd0003a5 -cpu MHz : 2300.000 -flags : fpu vme avx avx2 avx512f -"#; - - let info = parse_proc_cpuinfo(content).unwrap(); - - assert_eq!(info.vendor, "GenuineIntel"); - assert!(info.model.contains("Xeon")); - assert_eq!(info.family, Some(6)); - assert_eq!(info.model_number, Some(106)); - assert_eq!(info.stepping, Some(6)); - assert!(info.flags.contains(&"avx512f".to_string())); - } - - #[test] - fn test_parse_proc_cpuinfo_arm() { - let content = r#" -processor : 0 -BogoMIPS : 50.00 -Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 -CPU implementer : 0x41 -CPU architecture: 8 -CPU variant : 0x3 -CPU part : 0xd0c -"#; - - let info = parse_proc_cpuinfo(content).unwrap(); - - assert_eq!(info.vendor, "ARM"); - assert_eq!(info.microarchitecture, Some("Neoverse N1".to_string())); - assert!(info.flags.contains(&"asimd".to_string())); - } -} -``` - ---- - -## Step 3: GPU Enhancements - -### 3.1 Add GpuVendor Enum - -**File:** `src/domain/entities.rs` - -**Where:** Add before the GpuDevice struct - -**LeetCode Pattern:** PCI vendor ID lookup is a **Hash Map** problem (LC #1 Two Sum). -We're mapping a key (vendor ID) to a value (vendor enum). - -```rust -// ============================================================================= -// GPU VENDOR ENUM -// ============================================================================= -// -// WHY: Different GPU vendors have different detection methods: -// - NVIDIA: NVML, nvidia-smi -// - AMD: ROCm, rocm-smi -// - Intel: sysfs -// -// LEETCODE CONNECTION: This is lookup/categorization like: -// - LC #1 Two Sum: lookup by key -// - LC #49 Group Anagrams: group by category -// ============================================================================= - -/// GPU vendor classification. -/// -/// Used to determine which detection method to use and -/// which vendor-specific features are available. -/// -/// # PCI Vendor IDs -/// -/// | Vendor | PCI ID | -/// |--------|--------| -/// | NVIDIA | 0x10de | -/// | AMD | 0x1002 | -/// | Intel | 0x8086 | -/// -/// # References -/// -/// - [PCI Vendor IDs](https://pci-ids.ucw.cz/) -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -pub enum GpuVendor { - /// NVIDIA Corporation (PCI vendor 0x10de). - /// - /// Detection: NVML, nvidia-smi - /// Features: CUDA, compute capability - Nvidia, - - /// Advanced Micro Devices (PCI vendor 0x1002). - /// - /// Detection: ROCm SMI, sysfs - /// Features: ROCm, HIP - Amd, - - /// Intel Corporation (PCI vendor 0x8086). - /// - /// Detection: sysfs, Intel GPU tools - /// Features: OpenCL, Level Zero - Intel, - - /// Apple Inc. (integrated GPUs on Apple Silicon). - /// - /// Detection: system_profiler - /// Features: Metal - Apple, - - /// Unknown or unrecognized vendor. - Unknown, -} - -impl Default for GpuVendor { - fn default() -> Self { - GpuVendor::Unknown - } -} - -impl GpuVendor { - /// Create GpuVendor from PCI vendor ID. - /// - /// # Arguments - /// - /// * `vendor_id` - PCI vendor ID (e.g., "10de", "0x10de") - /// - /// # Example - /// - /// ```rust - /// use hardware_report::GpuVendor; - /// - /// assert_eq!(GpuVendor::from_pci_vendor("10de"), GpuVendor::Nvidia); - /// assert_eq!(GpuVendor::from_pci_vendor("0x1002"), GpuVendor::Amd); - /// assert_eq!(GpuVendor::from_pci_vendor("8086"), GpuVendor::Intel); - /// ``` - /// - /// # LeetCode Pattern - /// - /// This is a simple hash lookup - O(1) time. - /// Similar to LC #1 Two Sum where you look up complement in a map. - pub fn from_pci_vendor(vendor_id: &str) -> Self { - // Normalize: remove "0x" prefix, convert to lowercase - let normalized = vendor_id.trim().to_lowercase(); - let id = normalized.strip_prefix("0x").unwrap_or(&normalized); - - match id { - "10de" => GpuVendor::Nvidia, - "1002" => GpuVendor::Amd, - "8086" => GpuVendor::Intel, - _ => GpuVendor::Unknown, - } - } - - /// Get the vendor name as string. - pub fn name(&self) -> &'static str { - match self { - GpuVendor::Nvidia => "NVIDIA", - GpuVendor::Amd => "AMD", - GpuVendor::Intel => "Intel", - GpuVendor::Apple => "Apple", - GpuVendor::Unknown => "Unknown", - } - } -} - -impl std::fmt::Display for GpuVendor { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.name()) - } -} -``` - -### 3.2 Update GpuDevice Struct - -**File:** `src/domain/entities.rs` - -**Where:** Replace the existing `GpuDevice` struct - -```rust -// ============================================================================= -// GPU DEVICE STRUCT -// ============================================================================= -// -// WHY THE CHANGES: -// OLD: memory: String ("80 GB") - can't parse! -// NEW: memory_total_mb: u64 (81920) - math works! -// -// DETECTION METHODS (Chain of Responsibility): -// 1. NVML (native library) - most accurate -// 2. nvidia-smi (command) - fallback for NVIDIA -// 3. rocm-smi (command) - AMD GPUs -// 4. sysfs /sys/class/drm - universal Linux -// 5. lspci - basic enumeration -// 6. sysinfo - cross-platform fallback -// ============================================================================= - -/// GPU device information. -/// -/// Represents a discrete or integrated GPU with comprehensive metadata. -/// -/// # Memory Format Change (v0.2.0) -/// -/// **BREAKING CHANGE**: Memory is now numeric! -/// -/// ```rust -/// // OLD (v0.1.x) - String that couldn't be parsed -/// let memory: &str = &gpu.memory; // "80 GB" -/// let mb: u64 = memory.parse().unwrap(); // FAILS! -/// -/// // NEW (v0.2.0) - Numeric, just works -/// let memory_mb: u64 = gpu.memory_total_mb; // 81920 -/// let memory_gb: f64 = memory_mb as f64 / 1024.0; // 80.0 -/// ``` -/// -/// # Detection Methods -/// -/// GPUs are detected using multiple methods: -/// -/// | Priority | Method | Vendor | Memory | Driver | -/// |----------|--------|--------|--------|--------| -/// | 1 | NVML | NVIDIA | Yes | Yes | -/// | 2 | nvidia-smi | NVIDIA | Yes | Yes | -/// | 3 | rocm-smi | AMD | Yes | Yes | -/// | 4 | sysfs DRM | All | Varies | No | -/// | 5 | lspci | All | No | No | -/// -/// # Example -/// -/// ```rust -/// use hardware_report::{GpuDevice, GpuVendor}; -/// -/// // Calculate total GPU memory across all GPUs -/// let gpus: Vec = get_gpus(); -/// let total_memory_gb: f64 = gpus.iter() -/// .map(|g| g.memory_total_mb as f64 / 1024.0) -/// .sum(); -/// -/// // Filter NVIDIA GPUs -/// let nvidia_gpus: Vec<_> = gpus.iter() -/// .filter(|g| g.vendor == GpuVendor::Nvidia) -/// .collect(); -/// ``` -/// -/// # References -/// -/// - [NVIDIA NVML](https://docs.nvidia.com/deploy/nvml-api/) -/// - [AMD ROCm SMI](https://rocm.docs.amd.com/projects/rocm_smi_lib/en/latest/) -/// - [Linux DRM](https://www.kernel.org/doc/html/latest/gpu/drm-uapi.html) -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct GpuDevice { - // ========================================================================= - // IDENTIFICATION - // ========================================================================= - - /// GPU index (0-based, unique per system). - pub index: u32, - - /// GPU product name. - /// - /// Examples: - /// - "NVIDIA H100 80GB HBM3" - /// - "AMD Instinct MI250X" - /// - "Intel Arc A770" - pub name: String, - - /// GPU UUID (globally unique identifier). - /// - /// NVIDIA format: "GPU-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - pub uuid: String, - - // ========================================================================= - // MEMORY (THE BIG FIX!) - // ========================================================================= - // - // LEETCODE CONNECTION: Having numeric types enables all the math: - // - LC #1 Two Sum: can now sum GPU memory - // - LC #215 Kth Largest: can sort by memory - // ========================================================================= - - /// Total GPU memory in megabytes. - /// - /// **PRIMARY FIELD** - use this for calculations! - /// - /// Examples: - /// - H100 80GB: 81920 MB - /// - A100 40GB: 40960 MB - /// - RTX 4090: 24576 MB - #[serde(default)] - pub memory_total_mb: u64, - - /// Free GPU memory in megabytes (runtime value). - /// - /// Returns None if not queryable (e.g., lspci-only detection). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub memory_free_mb: Option, - - /// Used GPU memory in megabytes. - /// - /// Calculated as: total - free - #[serde(default, skip_serializing_if = "Option::is_none")] - pub memory_used_mb: Option, - - /// Legacy memory as string (DEPRECATED). - /// - /// Kept for backward compatibility. Use `memory_total_mb` instead. - #[deprecated(since = "0.2.0", note = "Use memory_total_mb instead")] - pub memory: String, - - // ========================================================================= - // PCI INFORMATION - // ========================================================================= - - /// PCI vendor:device ID (e.g., "10de:2330"). - /// - /// Format: `{vendor_id}:{device_id}` in lowercase hex. - pub pci_id: String, - - /// PCI bus address (e.g., "0000:01:00.0"). - /// - /// Format: `{domain}:{bus}:{device}.{function}` - /// Useful for NUMA correlation. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub pci_bus_id: Option, - - // ========================================================================= - // VENDOR INFORMATION - // ========================================================================= - - /// GPU vendor enum. - /// - /// Use for programmatic comparisons. - #[serde(default)] - pub vendor: GpuVendor, - - /// Vendor name as string. - /// - /// For display and backward compatibility. - #[serde(default)] - pub vendor_name: String, - - // ========================================================================= - // DRIVER AND CAPABILITIES - // ========================================================================= - - /// GPU driver version. - /// - /// NVIDIA example: "535.129.03" - /// AMD example: "6.3.6" - #[serde(default, skip_serializing_if = "Option::is_none")] - pub driver_version: Option, - - /// CUDA compute capability (NVIDIA only). - /// - /// Format: "major.minor" - /// Examples: "9.0" (Hopper), "8.9" (Ada), "8.0" (Ampere) - /// - /// # References - /// - /// - [CUDA Compute Capability](https://developer.nvidia.com/cuda-gpus) - #[serde(default, skip_serializing_if = "Option::is_none")] - pub compute_capability: Option, - - /// GPU architecture name. - /// - /// Examples: - /// - NVIDIA: "Hopper", "Ada Lovelace", "Ampere" - /// - AMD: "CDNA2", "RDNA3" - #[serde(default, skip_serializing_if = "Option::is_none")] - pub architecture: Option, - - // ========================================================================= - // TOPOLOGY - // ========================================================================= - - /// NUMA node affinity. - /// - /// Which NUMA node this GPU is attached to. - /// Important for optimal CPU-GPU data transfer. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub numa_node: Option, - - // ========================================================================= - // RUNTIME METRICS (Optional) - // ========================================================================= - - /// Current temperature in Celsius. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub temperature_celsius: Option, - - /// Power limit in watts. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub power_limit_watts: Option, - - /// Current power usage in watts. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub power_usage_watts: Option, - - /// GPU utilization percentage (0-100). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub utilization_percent: Option, - - // ========================================================================= - // METADATA - // ========================================================================= - - /// Detection method that discovered this GPU. - /// - /// Values: "nvml", "nvidia-smi", "rocm-smi", "sysfs", "lspci", "sysinfo" - #[serde(default)] - pub detection_method: String, -} - -impl Default for GpuDevice { - fn default() -> Self { - Self { - index: 0, - name: String::new(), - uuid: String::new(), - memory_total_mb: 0, - memory_free_mb: None, - memory_used_mb: None, - #[allow(deprecated)] - memory: String::new(), - pci_id: String::new(), - pci_bus_id: None, - vendor: GpuVendor::Unknown, - vendor_name: "Unknown".to_string(), - driver_version: None, - compute_capability: None, - architecture: None, - numa_node: None, - temperature_celsius: None, - power_limit_watts: None, - power_usage_watts: None, - utilization_percent: None, - detection_method: String::new(), - } - } -} - -impl GpuDevice { - /// Set the legacy memory string from memory_total_mb. - #[allow(deprecated)] - pub fn set_memory_string(&mut self) { - if self.memory_total_mb > 0 { - let gb = self.memory_total_mb as f64 / 1024.0; - if gb >= 1.0 { - self.memory = format!("{:.0} GB", gb); - } else { - self.memory = format!("{} MB", self.memory_total_mb); - } - } - } - - /// Calculate memory_used_mb from total and free. - pub fn calculate_memory_used(&mut self) { - if let Some(free) = self.memory_free_mb { - if self.memory_total_mb >= free { - self.memory_used_mb = Some(self.memory_total_mb - free); - } - } - } -} -``` - -### 3.3 Create GPU Parser Module - -**File:** `src/domain/parsers/gpu.rs` (NEW FILE) - -```rust -// ============================================================================= -// GPU PARSING MODULE -// ============================================================================= -// -// This module contains PURE FUNCTIONS for parsing GPU information -// from various sources. -// -// ARCHITECTURE: Domain layer - no I/O, no side effects -// ============================================================================= - -//! GPU information parsing functions. -//! -//! Pure parsing functions for GPU data from nvidia-smi, rocm-smi, lspci, etc. -//! -//! # Supported Formats -//! -//! - nvidia-smi CSV output -//! - rocm-smi JSON output -//! - lspci text output -//! -//! # References -//! -//! - [nvidia-smi](https://developer.nvidia.com/nvidia-system-management-interface) -//! - [rocm-smi](https://rocm.docs.amd.com/projects/rocm_smi_lib/en/latest/) - -use crate::domain::{GpuDevice, GpuVendor}; - -// ============================================================================= -// NVIDIA-SMI PARSING -// ============================================================================= -// -// LEETCODE CONNECTION: CSV parsing is like: -// - LC #722 Remove Comments: process structured text -// - LC #468 Validate IP Address: parse delimited fields -// -// Pattern: Split by delimiter, extract fields by position -// ============================================================================= - -/// Parse nvidia-smi CSV output into GPU devices. -/// -/// # Command -/// -/// ```bash -/// nvidia-smi --query-gpu=index,name,uuid,memory.total,memory.free,pci.bus_id,driver_version,compute_cap \ -/// --format=csv,noheader,nounits -/// ``` -/// -/// # Expected Format -/// -/// ```text -/// 0, NVIDIA H100 80GB HBM3, GPU-xxxx, 81920, 81000, 00000000:01:00.0, 535.129.03, 9.0 -/// 1, NVIDIA H100 80GB HBM3, GPU-yyyy, 81920, 80500, 00000000:02:00.0, 535.129.03, 9.0 -/// ``` -/// -/// # Fields -/// -/// 0. index -/// 1. name -/// 2. uuid -/// 3. memory.total (MiB, without units) -/// 4. memory.free (MiB) -/// 5. pci.bus_id -/// 6. driver_version -/// 7. compute_cap -/// -/// # Example -/// -/// ```rust -/// use hardware_report::domain::parsers::gpu::parse_nvidia_smi_output; -/// -/// let output = "0, NVIDIA H100, GPU-xxx, 81920, 81000, 00:01:00.0, 535.129.03, 9.0"; -/// let gpus = parse_nvidia_smi_output(output).unwrap(); -/// assert_eq!(gpus[0].memory_total_mb, 81920); -/// ``` -pub fn parse_nvidia_smi_output(output: &str) -> Result, String> { - let mut devices = Vec::new(); - - for line in output.lines() { - let line = line.trim(); - if line.is_empty() { - continue; - } - - // Split by comma - // LEETCODE: This is like parsing CSV - split and extract - let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect(); - - // Need at least 4 fields (index, name, uuid, memory) - if parts.len() < 4 { - continue; - } - - // Parse index - let index: u32 = parts[0].parse().unwrap_or(devices.len() as u32); - - // Parse memory (nvidia-smi with nounits gives MiB directly) - let memory_total_mb: u64 = parts.get(3) - .and_then(|s| s.parse().ok()) - .unwrap_or(0); - - let memory_free_mb: Option = parts.get(4) - .and_then(|s| s.parse().ok()); - - let mut device = GpuDevice { - index, - name: parts.get(1).unwrap_or(&"").to_string(), - uuid: parts.get(2).unwrap_or(&"").to_string(), - memory_total_mb, - memory_free_mb, - pci_bus_id: parts.get(5).map(|s| s.to_string()), - driver_version: parts.get(6).map(|s| s.to_string()), - compute_capability: parts.get(7).map(|s| s.to_string()), - vendor: GpuVendor::Nvidia, - vendor_name: "NVIDIA".to_string(), - detection_method: "nvidia-smi".to_string(), - ..Default::default() - }; - - // Set legacy fields - device.set_memory_string(); - device.calculate_memory_used(); - - // Build PCI ID from bus ID if possible - // Bus ID format: 00000000:01:00.0 - // We'd need device ID from somewhere else for full pci_id - - devices.push(device); - } - - Ok(devices) -} - -// ============================================================================= -// LSPCI PARSING -// ============================================================================= -// -// LEETCODE CONNECTION: This is pattern matching in strings: -// - LC #28 Find Index of First Occurrence -// - LC #10 Regular Expression Matching -// -// We scan for GPU-related PCI class codes -// ============================================================================= - -/// Parse lspci output for GPU devices. -/// -/// # Command -/// -/// ```bash -/// lspci -nn -/// ``` -/// -/// # Expected Format -/// -/// ```text -/// 01:00.0 3D controller [0302]: NVIDIA Corporation GH100 [H100] [10de:2330] (rev a1) -/// 02:00.0 VGA compatible controller [0300]: Advanced Micro Devices [1002:73bf] -/// ``` -/// -/// # PCI Class Codes -/// -/// - 0300: VGA compatible controller -/// - 0302: 3D controller (NVIDIA compute GPUs) -/// - 0380: Display controller -/// -/// # Limitations -/// -/// lspci does NOT provide: -/// - GPU memory (returns 0) -/// - Driver version -/// - UUID -/// -/// Use this as fallback enumeration only. -pub fn parse_lspci_gpu_output(output: &str) -> Result, String> { - let mut devices = Vec::new(); - let mut gpu_index = 0; - - for line in output.lines() { - let line_lower = line.to_lowercase(); - - // Check for GPU-related PCI classes - // [0300] = VGA controller - // [0302] = 3D controller - // [0380] = Display controller - let is_gpu = line_lower.contains("[0300]") - || line_lower.contains("[0302]") - || line_lower.contains("[0380]") - || line_lower.contains("vga compatible") - || line_lower.contains("3d controller") - || line_lower.contains("display controller"); - - if !is_gpu { - continue; - } - - // Extract PCI bus ID (first field) - // Format: "01:00.0 3D controller..." - let pci_bus_id = line.split_whitespace().next().map(String::from); - - // Extract vendor:device ID - // Look for pattern [xxxx:yyyy] - let pci_id = extract_pci_id(line); - - // Determine vendor from PCI ID - let vendor = pci_id.as_ref() - .map(|id| { - let vendor_id = id.split(':').next().unwrap_or(""); - GpuVendor::from_pci_vendor(vendor_id) - }) - .unwrap_or(GpuVendor::Unknown); - - // Extract name (everything between class and PCI ID) - let name = extract_gpu_name_from_lspci(line); - - let device = GpuDevice { - index: gpu_index, - name, - uuid: format!("pci-{}", pci_bus_id.as_deref().unwrap_or("unknown")), - pci_id: pci_id.unwrap_or_default(), - pci_bus_id, - vendor: vendor.clone(), - vendor_name: vendor.name().to_string(), - detection_method: "lspci".to_string(), - // NOTE: lspci cannot determine memory! - memory_total_mb: 0, - ..Default::default() - }; - - devices.push(device); - gpu_index += 1; - } - - Ok(devices) -} - -/// Extract PCI vendor:device ID from lspci line. -/// -/// Looks for pattern `[xxxx:yyyy]` at end of line. -fn extract_pci_id(line: &str) -> Option { - // Find the last occurrence of [xxxx:yyyy] - // LEETCODE: This is like LC #28 - finding a pattern - - let mut result = None; - let mut remaining = line; - - while let Some(start) = remaining.find('[') { - if let Some(end) = remaining[start..].find(']') { - let bracket_content = &remaining[start+1..start+end]; - - // Check if it looks like a PCI ID (xxxx:yyyy) - if bracket_content.len() == 9 && bracket_content.chars().nth(4) == Some(':') { - // Verify it's hex - let parts: Vec<&str> = bracket_content.split(':').collect(); - if parts.len() == 2 - && parts[0].chars().all(|c| c.is_ascii_hexdigit()) - && parts[1].chars().all(|c| c.is_ascii_hexdigit()) - { - result = Some(bracket_content.to_lowercase()); - } - } - - remaining = &remaining[start+end+1..]; - } else { - break; - } - } - - result -} - -/// Extract GPU name from lspci line. -fn extract_gpu_name_from_lspci(line: &str) -> String { - // Try to find the name between the class description and PCI IDs - // "01:00.0 3D controller [0302]: NVIDIA Corporation GH100 [H100] [10de:2330]" - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - if let Some(colon_pos) = line.find("]:") { - let after_class = &line[colon_pos + 2..].trim(); - - // Find where PCI IDs start (last [xxxx:yyyy]) - if let Some(pci_start) = after_class.rfind('[') { - let name = after_class[..pci_start].trim(); - // Remove trailing [device name] brackets too - if let Some(name_end) = name.rfind('[') { - return name[..name_end].trim().to_string(); - } - return name.to_string(); - } - return after_class.to_string(); - } - - line.to_string() -} - -// ============================================================================= -// PCI VENDOR LOOKUP -// ============================================================================= - -/// Parse PCI vendor ID to GpuVendor. -/// -/// # Arguments -/// -/// * `vendor_id` - Hex string (e.g., "10de", "0x10de") -/// -/// # Example -/// -/// ```rust -/// use hardware_report::domain::parsers::gpu::parse_pci_vendor; -/// use hardware_report::GpuVendor; -/// -/// assert_eq!(parse_pci_vendor("10de"), GpuVendor::Nvidia); -/// assert_eq!(parse_pci_vendor("0x1002"), GpuVendor::Amd); -/// ``` -pub fn parse_pci_vendor(vendor_id: &str) -> GpuVendor { - GpuVendor::from_pci_vendor(vendor_id) -} - -// ============================================================================= -// UNIT TESTS -// ============================================================================= - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_nvidia_smi_output() { - let output = r#"0, NVIDIA H100 80GB HBM3, GPU-12345678-1234-1234-1234-123456789abc, 81920, 81000, 00000000:01:00.0, 535.129.03, 9.0 -1, NVIDIA H100 80GB HBM3, GPU-87654321-4321-4321-4321-cba987654321, 81920, 80500, 00000000:02:00.0, 535.129.03, 9.0"#; - - let gpus = parse_nvidia_smi_output(output).unwrap(); - - assert_eq!(gpus.len(), 2); - assert_eq!(gpus[0].index, 0); - assert_eq!(gpus[0].name, "NVIDIA H100 80GB HBM3"); - assert_eq!(gpus[0].memory_total_mb, 81920); - assert_eq!(gpus[0].memory_free_mb, Some(81000)); - assert_eq!(gpus[0].driver_version, Some("535.129.03".to_string())); - assert_eq!(gpus[0].compute_capability, Some("9.0".to_string())); - assert_eq!(gpus[0].vendor, GpuVendor::Nvidia); - } - - #[test] - fn test_parse_nvidia_smi_empty() { - let output = ""; - let gpus = parse_nvidia_smi_output(output).unwrap(); - assert!(gpus.is_empty()); - } - - #[test] - fn test_parse_lspci_gpu_output() { - let output = r#" -00:02.0 VGA compatible controller [0300]: Intel Corporation Device [8086:9a49] (rev 01) -01:00.0 3D controller [0302]: NVIDIA Corporation GH100 [H100 SXM5 80GB] [10de:2330] (rev a1) -02:00.0 3D controller [0302]: NVIDIA Corporation GH100 [H100 SXM5 80GB] [10de:2330] (rev a1) -"#; - - let gpus = parse_lspci_gpu_output(output).unwrap(); - - assert_eq!(gpus.len(), 3); - - // Intel GPU - assert_eq!(gpus[0].vendor, GpuVendor::Intel); - assert_eq!(gpus[0].pci_id, "8086:9a49"); - - // NVIDIA GPUs - assert_eq!(gpus[1].vendor, GpuVendor::Nvidia); - assert_eq!(gpus[1].pci_id, "10de:2330"); - assert_eq!(gpus[1].pci_bus_id, Some("01:00.0".to_string())); - } - - #[test] - fn test_extract_pci_id() { - assert_eq!( - extract_pci_id("...controller [0302]: NVIDIA [10de:2330] (rev a1)"), - Some("10de:2330".to_string()) - ); - assert_eq!( - extract_pci_id("...controller [0300]: Intel [8086:9a49]"), - Some("8086:9a49".to_string()) - ); - assert_eq!(extract_pci_id("no pci id here"), None); - } - - #[test] - fn test_parse_pci_vendor() { - assert_eq!(parse_pci_vendor("10de"), GpuVendor::Nvidia); - assert_eq!(parse_pci_vendor("0x10de"), GpuVendor::Nvidia); - assert_eq!(parse_pci_vendor("1002"), GpuVendor::Amd); - assert_eq!(parse_pci_vendor("8086"), GpuVendor::Intel); - assert_eq!(parse_pci_vendor("unknown"), GpuVendor::Unknown); - } -} -``` - ---- - -## Step 4: Memory Enhancements - -### 4.1 Update MemoryModule Struct - -**File:** `src/domain/entities.rs` - -**Where:** Update the existing `MemoryModule` struct - -```rust -// ============================================================================= -// MEMORY MODULE STRUCT -// ============================================================================= -// -// KEY ADDITION: part_number field for asset tracking! -// -// CMDB use case: "Which exact memory do we need to order for replacement?" -// Answer: Look up the part_number and order that exact module. -// ============================================================================= - -/// Memory technology type. -/// -/// # References -/// -/// - [JEDEC Standards](https://www.jedec.org/) -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] -pub enum MemoryType { - Ddr3, - Ddr4, - Ddr5, - Lpddr4, - Lpddr5, - Hbm2, - Hbm3, - #[default] - Unknown, -} - -impl MemoryType { - /// Parse memory type from string. - pub fn from_string(type_str: &str) -> Self { - match type_str.to_uppercase().as_str() { - "DDR3" => MemoryType::Ddr3, - "DDR4" => MemoryType::Ddr4, - "DDR5" => MemoryType::Ddr5, - "LPDDR4" | "LPDDR4X" => MemoryType::Lpddr4, - "LPDDR5" | "LPDDR5X" => MemoryType::Lpddr5, - "HBM2" | "HBM2E" => MemoryType::Hbm2, - "HBM3" | "HBM3E" => MemoryType::Hbm3, - _ => MemoryType::Unknown, - } - } -} - -/// Individual memory module (DIMM). -/// -/// # New Fields (v0.2.0) -/// -/// - `part_number` - Manufacturer part number for ordering -/// - `size_bytes` - Numeric size for calculations -/// - `speed_mhz` - Numeric speed for comparisons -/// -/// # Example -/// -/// ```rust -/// use hardware_report::MemoryModule; -/// -/// // Calculate total memory -/// let total_gb: f64 = modules.iter() -/// .map(|m| m.size_bytes as f64 / (1024.0 * 1024.0 * 1024.0)) -/// .sum(); -/// -/// // Find part number for ordering replacement -/// let part = modules[0].part_number.as_deref().unwrap_or("Unknown"); -/// println!("Order part: {}", part); -/// ``` -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct MemoryModule { - /// Physical slot location (e.g., "DIMM_A1", "ChannelA-DIMM0"). - pub location: String, - - /// Bank locator (e.g., "BANK 0", "P0 CHANNEL A"). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub bank_locator: Option, - - /// Module size in bytes. - /// - /// PRIMARY SIZE FIELD - use for calculations. - #[serde(default)] - pub size_bytes: u64, - - /// Module size as string (e.g., "32 GB"). - /// - /// For display and backward compatibility. - pub size: String, - - /// Memory type enum. - #[serde(default)] - pub memory_type: MemoryType, - - /// Memory type as string (e.g., "DDR4", "DDR5"). - #[serde(rename = "type")] - pub type_: String, - - /// Speed in MT/s (megatransfers per second). - /// - /// DDR4-3200 = 3200 MT/s - #[serde(default, skip_serializing_if = "Option::is_none")] - pub speed_mts: Option, - - /// Configured clock speed in MHz. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub speed_mhz: Option, - - /// Speed as string (e.g., "3200 MT/s"). - pub speed: String, - - /// Manufacturer name (e.g., "Samsung", "Micron", "SK Hynix"). - pub manufacturer: String, - - /// Module serial number. - pub serial: String, - - /// Manufacturer part number. - /// - /// **IMPORTANT** for procurement and warranty! - /// - /// Examples: - /// - "M393A4K40EB3-CWE" (Samsung 32GB DDR4-3200) - /// - "MTA36ASF8G72PZ-3G2E1" (Micron 64GB DDR4-3200) - #[serde(default, skip_serializing_if = "Option::is_none")] - pub part_number: Option, - - /// Number of memory ranks (1, 2, 4, or 8). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub rank: Option, - - /// Data width in bits (64, 72 for ECC). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub data_width_bits: Option, - - /// Whether ECC is supported. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub ecc: Option, - - /// Configured voltage in volts. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub voltage: Option, -} - -impl Default for MemoryModule { - fn default() -> Self { - Self { - location: String::new(), - bank_locator: None, - size_bytes: 0, - size: String::new(), - memory_type: MemoryType::Unknown, - type_: String::new(), - speed_mts: None, - speed_mhz: None, - speed: String::new(), - manufacturer: String::new(), - serial: String::new(), - part_number: None, - rank: None, - data_width_bits: None, - ecc: None, - voltage: None, - } - } -} -``` - ---- - -## Step 5: Network Enhancements - -### 5.1 Update NetworkInterface Struct - -**File:** `src/domain/entities.rs` - -```rust -// ============================================================================= -// NETWORK INTERFACE TYPE ENUM -// ============================================================================= - -/// Network interface type classification. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] -pub enum NetworkInterfaceType { - Ethernet, - Wireless, - Loopback, - Bridge, - Vlan, - Bond, - Veth, - TunTap, - Infiniband, - #[default] - Unknown, -} - -/// Network interface information. -/// -/// # New Fields (v0.2.0) -/// -/// - `driver` / `driver_version` - For compatibility tracking -/// - `speed_mbps` - Numeric speed -/// - `mtu` - Maximum transmission unit -/// - `is_up` / `is_virtual` - State flags -/// -/// # Example -/// -/// ```rust -/// // Find all 10G+ physical interfaces that are up -/// let fast_nics: Vec<_> = interfaces.iter() -/// .filter(|i| i.is_up && !i.is_virtual && i.speed_mbps.unwrap_or(0) >= 10000) -/// .collect(); -/// ``` -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct NetworkInterface { - /// Interface name (e.g., "eth0", "ens192"). - pub name: String, - - /// MAC address (e.g., "00:11:22:33:44:55"). - pub mac: String, - - /// Primary IPv4 address. - pub ip: String, - - /// Network prefix length (e.g., "24"). - pub prefix: String, - - /// Link speed in Mbps. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub speed_mbps: Option, - - /// Link speed as string. - pub speed: Option, - - /// Interface type enum. - #[serde(default)] - pub interface_type: NetworkInterfaceType, - - /// Interface type as string. - #[serde(rename = "type")] - pub type_: String, - - /// Hardware vendor name. - pub vendor: String, - - /// Hardware model. - pub model: String, - - /// PCI vendor:device ID. - pub pci_id: String, - - /// NUMA node affinity. - pub numa_node: Option, - - /// Kernel driver in use (e.g., "igb", "mlx5_core"). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub driver: Option, - - /// Driver version. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub driver_version: Option, - - /// Firmware version. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub firmware_version: Option, - - /// Maximum Transmission Unit in bytes. - #[serde(default)] - pub mtu: u32, - - /// Whether interface is operationally up. - #[serde(default)] - pub is_up: bool, - - /// Whether this is a virtual interface. - #[serde(default)] - pub is_virtual: bool, - - /// Link detected (carrier present). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub carrier: Option, - - /// Duplex mode ("full", "half"). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub duplex: Option, -} - -impl Default for NetworkInterface { - fn default() -> Self { - Self { - name: String::new(), - mac: String::new(), - ip: String::new(), - prefix: String::new(), - speed_mbps: None, - speed: None, - interface_type: NetworkInterfaceType::Unknown, - type_: String::new(), - vendor: String::new(), - model: String::new(), - pci_id: String::new(), - numa_node: None, - driver: None, - driver_version: None, - firmware_version: None, - mtu: 1500, - is_up: false, - is_virtual: false, - carrier: None, - duplex: None, - } - } -} -``` - ---- - -## Step 6: Update Cargo.toml - -**File:** `Cargo.toml` - -Add feature flags for optional dependencies: - -```toml -[package] -name = "hardware_report" -version = "0.2.0" # Bump version for breaking changes -edition = "2021" -authors = ["Kenny Sheridan"] -description = "A tool for generating hardware information reports" - -[dependencies] -# Existing dependencies... -lazy_static = "1.4" -tonic = "0.10" -reqwest = { version = "0.11", features = ["json"] } -structopt = "0.3" -sysinfo = "0.32.0" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -clap = { version = "4.4", features = ["derive"] } -thiserror = "1.0" -log = "0.4" -env_logger = "0.11.5" -regex = "1.11.1" -toml = "0.8.19" -libc = "0.2.161" -tokio = { version = "1.0", features = ["full"] } -async-trait = "0.1" - -# NEW: Optional NVIDIA GPU support via NVML -# Requires NVIDIA driver at runtime -nvml-wrapper = { version = "0.9", optional = true } - -# x86-specific CPU detection (only on x86/x86_64) -[target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies] -raw-cpuid = { version = "11", optional = true } - -[features] -default = [] -nvidia = ["nvml-wrapper"] -x86-cpu = ["raw-cpuid"] -full = ["nvidia", "x86-cpu"] - -[dev-dependencies] -tempfile = "3.8" -assert_fs = "1.0" -predicates = "3.0" - -[profile.release] -opt-level = 3 -lto = true -codegen-units = 1 -strip = true - -[lib] -name = "hardware_report" -path = "src/lib.rs" - -[[bin]] -name = "hardware_report" -path = "src/bin/hardware_report.rs" -``` - ---- - -## Summary: Implementation Checklist - -Use this checklist as you implement: - -### Entities (`src/domain/entities.rs`) - -- [ ] Add `StorageType` enum (after line 205) -- [ ] Update `StorageDevice` struct with new fields -- [ ] Add `Default` impl for `StorageType` -- [ ] Add `Default` impl for `StorageDevice` -- [ ] Add `CpuCacheInfo` struct -- [ ] Update `CpuInfo` struct with new fields -- [ ] Add `GpuVendor` enum -- [ ] Update `GpuDevice` struct with numeric memory -- [ ] Update `MemoryModule` with `part_number` -- [ ] Add `NetworkInterfaceType` enum -- [ ] Update `NetworkInterface` with driver fields - -### Parsers (`src/domain/parsers/`) - -- [ ] Update `storage.rs` with new functions -- [ ] Update `cpu.rs` with sysfs/cpuinfo parsing -- [ ] Create `gpu.rs` module (new file) -- [ ] Update `mod.rs` to export `gpu` - -### Adapters (`src/adapters/secondary/system/linux.rs`) - -- [ ] Update `get_storage_info` with sysfs detection -- [ ] Update `get_cpu_info` with frequency/cache -- [ ] Update `get_gpu_info` with multi-method detection -- [ ] Add helper methods for sysfs reading - -### Configuration - -- [ ] Update `Cargo.toml` with features -- [ ] Update version to `0.2.0` - -### Testing - -- [ ] Run `cargo check` after each change -- [ ] Run `cargo test` to verify parsers -- [ ] Test on ARM hardware if available - ---- - -## LeetCode Practice Problems - -To solidify your understanding of the patterns used, practice these: - -| Pattern | LeetCode Problem | Used In | -|---------|------------------|---------| -| Hash Map Lookup | LC #1 Two Sum | PCI vendor ID → vendor name | -| String Parsing | LC #8 atoi | sysfs frequency parsing | -| String Parsing | LC #468 Validate IP Address | MAC address validation | -| Tree Traversal | LC #102 Level Order | Cache hierarchy, sysfs directory walking | -| Merge | LC #88 Merge Sorted Array | Merging GPU info from multiple sources | -| Filter | LC #283 Move Zeroes | Filter virtual devices | -| Categorization | LC #49 Group Anagrams | StorageType, GpuVendor classification | -| State Machine | LC #65 Valid Number | Parsing multi-line /proc/cpuinfo | -| Pattern Matching | LC #28 Find Index | Extract PCI ID from lspci | - -Good luck with your implementation! Type it in yourself to learn the patterns. - - - -[{"id": "learn-1", "content": "Update ENHANCEMENTS.md with implementation guide overview", "status": "completed", "priority": "high"}, {"id": "learn-2", "content": "Update STORAGE_DETECTION.md with full implementation code", "status": "completed", "priority": "high"}, {"id": "learn-3", "content": "Update GPU_DETECTION.md with full implementation code", "status": "in_progress", "priority": "high"}, {"id": "learn-4", "content": "Update CPU_ENHANCEMENTS.md with full implementation code", "status": "pending", "priority": "high"}, {"id": "learn-5", "content": "Update MEMORY_ENHANCEMENTS.md with full implementation code", "status": "pending", "priority": "medium"}, {"id": "learn-6", "content": "Update NETWORK_ENHANCEMENTS.md with full implementation code", "status": "pending", "priority": "medium"}, {"id": "learn-7", "content": "Create step-by-step implementation checklist", "status": "pending", "priority": "high"}] \ No newline at end of file diff --git a/docs/LINUX_ADAPTER_IMPLEMENTATION.md b/docs/LINUX_ADAPTER_IMPLEMENTATION.md deleted file mode 100644 index 4279629..0000000 --- a/docs/LINUX_ADAPTER_IMPLEMENTATION.md +++ /dev/null @@ -1,1822 +0,0 @@ -# Linux Adapter Implementation Guide - -> **File:** `src/adapters/secondary/system/linux.rs` -> **Purpose:** Platform-specific hardware detection for Linux (x86_64 and aarch64) -> **Architecture:** Adapter layer in Hexagonal/Ports-and-Adapters pattern - -## Table of Contents - -1. [Overview](#overview) -2. [Architecture Context](#architecture-context) -3. [Prerequisites](#prerequisites) -4. [Implementation Steps](#implementation-steps) - - [Step 1: Update Imports](#step-1-update-imports) - - [Step 2: Storage Detection](#step-2-storage-detection) - - [Step 3: CPU Detection](#step-3-cpu-detection) - - [Step 4: GPU Detection](#step-4-gpu-detection) - - [Step 5: Network Detection](#step-5-network-detection) -5. [Helper Functions](#helper-functions) -6. [Testing](#testing) -7. [LeetCode Pattern Summary](#leetcode-pattern-summary) - ---- - -## Overview - -The `LinuxSystemInfoProvider` is an **adapter** that implements the `SystemInfoProvider` **port** (trait). It translates abstract hardware queries into Linux-specific operations (sysfs reads, command execution). - -### What Changes? - -| Method | Current | New | -|--------|---------|-----| -| `get_storage_info` | lsblk only | sysfs primary + lsblk enrichment + sysinfo fallback | -| `get_cpu_info` | lscpu + dmidecode | + sysfs for frequency/cache | -| `get_gpu_info` | nvidia-smi + lspci | + multi-method chain with numeric memory | -| `get_network_info` | ip command | + sysfs for driver/MTU/state | - ---- - -## Architecture Context - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ YOUR CODE CHANGES │ -└─────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ ADAPTER LAYER: src/adapters/secondary/system/linux.rs │ -│ │ -│ LinuxSystemInfoProvider │ -│ ├── get_storage_info() ← MODIFY: Add sysfs detection │ -│ ├── get_cpu_info() ← MODIFY: Add frequency/cache │ -│ ├── get_gpu_info() ← MODIFY: Multi-method + numeric memory │ -│ └── get_network_info() ← MODIFY: Add driver/MTU │ -│ │ -│ Helper methods (NEW): │ -│ ├── detect_storage_sysfs() │ -│ ├── detect_storage_lsblk() │ -│ ├── detect_cpu_sysfs_frequency() │ -│ ├── detect_cpu_sysfs_cache() │ -│ ├── detect_gpus_nvidia_smi() │ -│ ├── detect_gpus_lspci() │ -│ ├── detect_gpus_sysfs_drm() │ -│ ├── detect_network_sysfs() │ -│ ├── read_sysfs_file() │ -│ └── merge_*_info() │ -└─────────────────────────────────────────────────────────────────────┘ - │ - │ implements - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ PORT LAYER: src/ports/secondary/system.rs │ -│ │ -│ trait SystemInfoProvider { │ -│ fn get_storage_info() -> Result │ -│ fn get_cpu_info() -> Result │ -│ fn get_gpu_info() -> Result │ -│ fn get_network_info() -> Result │ -│ } │ -└─────────────────────────────────────────────────────────────────────┘ - │ - │ uses - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ DOMAIN LAYER: src/domain/ │ -│ │ -│ entities.rs - StorageDevice, CpuInfo, GpuDevice, etc. │ -│ parsers/ - Pure parsing functions (no I/O) │ -└─────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Prerequisites - -Before modifying `linux.rs`, ensure you have: - -1. **Updated `entities.rs`** with: - - `StorageType` enum - - Updated `StorageDevice` struct - - Updated `CpuInfo` struct with cache/frequency - - `GpuVendor` enum - - Updated `GpuDevice` struct with numeric memory - - Updated `NetworkInterface` struct - -2. **Updated `parsers/storage.rs`** with: - - `parse_sysfs_size()` - - `parse_sysfs_rotational()` - - `parse_lsblk_json()` - - `is_virtual_device()` - -3. **Created `parsers/gpu.rs`** with: - - `parse_nvidia_smi_output()` - - `parse_lspci_gpu_output()` - -4. **Updated `parsers/cpu.rs`** with: - - `parse_sysfs_freq_khz()` - - `parse_sysfs_cache_size()` - - `parse_proc_cpuinfo()` - ---- - -## Implementation Steps - -### Step 1: Update Imports - -**Location:** Top of `linux.rs` (lines 17-30) - -**Replace the existing imports with:** - -```rust -// ============================================================================= -// IMPORTS -// ============================================================================= -// -// ARCHITECTURE NOTE: -// - We import from `domain` (entities and parsers) -// - We import from `ports` (the trait we implement) -// - We DO NOT import from other adapters (adapters are independent) -// -// LEETCODE CONNECTION: Dependency management is like LC #210 Course Schedule II -// - There's an ordering: domain → ports → adapters -// - Circular dependencies would break the build -// ============================================================================= - -//! Linux system information provider -//! -//! Implements `SystemInfoProvider` for Linux systems using: -//! - sysfs (`/sys`) for direct kernel data -//! - procfs (`/proc`) for process/system info -//! - Command execution for tools like lsblk, nvidia-smi -//! -//! # Platform Support -//! -//! - x86_64: Full support including raw-cpuid -//! - aarch64: Full support via sysfs (ARM servers, DGX Spark) -//! -//! # Detection Strategy -//! -//! Each hardware type uses multiple detection methods: -//! 1. Primary: sysfs (most reliable, always available) -//! 2. Secondary: Command output (lsblk, nvidia-smi, etc.) -//! 3. Fallback: sysinfo crate (cross-platform) - -use crate::domain::{ - // Existing imports - keep these - combine_cpu_info, determine_memory_speed, determine_memory_type, - parse_dmidecode_bios_info, parse_dmidecode_chassis_info, parse_dmidecode_cpu, - parse_dmidecode_memory, parse_dmidecode_system_info, parse_free_output, - parse_hostname_output, parse_ip_output, parse_lscpu_output, - BiosInfo, ChassisInfo, MemoryInfo, MotherboardInfo, NumaNode, SystemInfo, - - // NEW imports for enhanced entities - CpuInfo, CpuCacheInfo, GpuInfo, GpuDevice, GpuVendor, - StorageInfo, StorageDevice, StorageType, - NetworkInfo, NetworkInterface, NetworkInterfaceType, - - // NEW imports for parsers - SystemError, -}; - -// NEW: Import parser functions -use crate::domain::parsers::storage::{ - parse_sysfs_size, parse_sysfs_rotational, parse_lsblk_json, is_virtual_device, -}; -use crate::domain::parsers::cpu::{ - parse_sysfs_freq_khz, parse_sysfs_cache_size, parse_proc_cpuinfo, -}; -use crate::domain::parsers::gpu::{ - parse_nvidia_smi_output, parse_lspci_gpu_output, -}; - -use crate::ports::{CommandExecutor, SystemCommand, SystemInfoProvider}; -use async_trait::async_trait; -use std::collections::HashMap; -use std::sync::Arc; -use std::time::Duration; - -// NEW: Standard library imports for sysfs reading -use std::fs; -use std::path::{Path, PathBuf}; -``` - ---- - -### Step 2: Storage Detection - -**Location:** Replace `get_storage_info` method (around line 153-170) - -**LeetCode Patterns:** -- **Chain of Responsibility**: Try sysfs → lsblk → sysinfo -- **Merge/Combine** (LC #88): Combine results from multiple sources -- **Tree Traversal** (LC #102): Walk /sys/block directory - -```rust -// ============================================================================= -// STORAGE DETECTION -// ============================================================================= -// -// PROBLEM SOLVED: -// - Old code used only lsblk, which fails on some ARM platforms -// - New code uses sysfs as primary (works everywhere on Linux) -// -// DETECTION CHAIN (Chain of Responsibility pattern): -// 1. sysfs /sys/block - Primary, most reliable -// 2. lsblk -J - Enrichment (WWN, transport type) -// 3. sysinfo crate - Fallback if above fail -// -// LEETCODE CONNECTION: -// - LC #88 Merge Sorted Array: we merge info from multiple sources -// - LC #200 Number of Islands: walking the sysfs "grid" -// ============================================================================= - -async fn get_storage_info(&self) -> Result { - // ========================================================================= - // STEP 1: Primary detection via sysfs - // ========================================================================= - // - // WHY SYSFS FIRST? - // - Direct kernel interface - always available on Linux - // - No external tools required (lsblk might not be installed) - // - Works identically on x86_64 and aarch64 - // - Doesn't require parsing command output (more reliable) - // - // sysfs structure: - // /sys/block/ - // ├── sda/ - // │ ├── size # Size in 512-byte sectors - // │ ├── queue/ - // │ │ └── rotational # 0=SSD, 1=HDD - // │ └── device/ - // │ ├── model # Device model - // │ └── serial # Serial number (may need root) - // ├── nvme0n1/ - // └── mmcblk0/ # eMMC on ARM - // ========================================================================= - - let mut devices = Vec::new(); - - // if let chaining - cleaner than match for "try or log warning" - if let Ok(sysfs_devices) = self.detect_storage_sysfs().await { - log::debug!("sysfs detected {} storage devices", sysfs_devices.len()); - devices = sysfs_devices; - } else { - log::warn!("sysfs storage detection failed, trying next method"); - } - - // ========================================================================= - // STEP 2: Enrichment via lsblk - // ========================================================================= - // - // Even if sysfs worked, lsblk may have additional data: - // - WWN (World Wide Name) - // - Transport type (nvme, sata, usb) - // - Serial (sometimes easier to get via lsblk) - // - // MERGE STRATEGY: - // - Match by device name - // - Fill in missing fields from lsblk - // - Don't overwrite existing data (sysfs is more reliable) - // - // LEETCODE CONNECTION: This is the merge pattern - // Similar to LC #88 Merge Sorted Array, but merging by key (device name) - // ========================================================================= - - if let Ok(lsblk_devices) = self.detect_storage_lsblk().await { - log::debug!( - "lsblk found {} devices for enrichment", - lsblk_devices.len() - ); - self.merge_storage_info(&mut devices, lsblk_devices); - } - - // ========================================================================= - // STEP 3: Fallback via sysinfo crate - // ========================================================================= - // - // If we still have no devices, something unusual is happening. - // Try sysinfo as a cross-platform fallback. - // - // This can happen in: - // - Containers with limited /sys access - // - Unusual system configurations - // ========================================================================= - - if devices.is_empty() { - log::warn!("No devices from sysfs/lsblk, trying sysinfo fallback"); - if let Ok(sysinfo_devices) = self.detect_storage_sysinfo() { - devices = sysinfo_devices; - } - } - - // ========================================================================= - // POST-PROCESSING - // ========================================================================= - // - // 1. Filter virtual devices (loop, ram, dm-*) - // 2. Ensure size fields are calculated - // 3. Sort for consistent output - // - // LEETCODE CONNECTION: - // - Filtering is like LC #283 Move Zeroes (filter in-place) - // - Sorting is standard LC pattern - // ========================================================================= - - // Filter out virtual devices - they're not physical hardware - // PATTERN: retain() is more efficient than filter() + collect() - devices.retain(|d| d.device_type != StorageType::Virtual); - - // Ensure all calculated fields are populated - for device in &mut devices { - if device.size_gb == 0.0 && device.size_bytes > 0 { - device.calculate_size_fields(); - } - device.set_device_path(); - } - - // Sort by name for consistent, predictable output - devices.sort_by(|a, b| a.name.cmp(&b.name)); - - log::info!("Detected {} storage devices", devices.len()); - Ok(StorageInfo { devices }) -} -``` - -**Add these helper methods to `impl LinuxSystemInfoProvider`:** - -```rust -// ============================================================================= -// STORAGE HELPER METHODS -// ============================================================================= - -impl LinuxSystemInfoProvider { - /// Detect storage devices via sysfs /sys/block. - /// - /// # How It Works - /// - /// 1. Read directory listing of /sys/block - /// 2. For each device, read attributes from sysfs files - /// 3. Build StorageDevice struct - /// - /// # sysfs Paths Used - /// - /// | Path | Content | Example | - /// |------|---------|---------| - /// | `/sys/block/{dev}/size` | Sectors (×512=bytes) | "3907029168" | - /// | `/sys/block/{dev}/queue/rotational` | 0=SSD, 1=HDD | "0" | - /// | `/sys/block/{dev}/device/model` | Model name | "Samsung SSD 980" | - /// | `/sys/block/{dev}/device/serial` | Serial (may need root) | "S5GXNF0N1234" | - /// - /// # LeetCode Connection - /// - /// This is **directory traversal** similar to: - /// - LC #200 Number of Islands (grid traversal) - /// - LC #130 Surrounded Regions - /// - LC #417 Pacific Atlantic Water Flow - /// - /// We're walking a tree structure (filesystem) and extracting data. - async fn detect_storage_sysfs(&self) -> Result, SystemError> { - let mut devices = Vec::new(); - - // Path to block devices in sysfs - let sys_block = Path::new("/sys/block"); - - // Check if sysfs is mounted/accessible - if !sys_block.exists() { - return Err(SystemError::NotAvailable { - resource: "/sys/block".to_string(), - }); - } - - // Read directory entries - // PATTERN: This is the "traversal" part - we visit each node (device) - let entries = fs::read_dir(sys_block).map_err(|e| { - SystemError::IoError { - path: "/sys/block".to_string(), - message: e.to_string(), - } - })?; - - // Process each block device - for entry in entries.flatten() { - let device_name = entry.file_name().to_string_lossy().to_string(); - - // ───────────────────────────────────────────────────────────── - // EARLY FILTERING: Skip virtual devices - // ───────────────────────────────────────────────────────────── - // WHY EARLY? Saves I/O - don't read attributes for devices we'll skip - // PATTERN: This is like LC #283 Move Zeroes - filter early - if is_virtual_device(&device_name) { - log::trace!("Skipping virtual device: {}", device_name); - continue; - } - - let device_path = entry.path(); - - // ───────────────────────────────────────────────────────────── - // READ SIZE (required field) - // ───────────────────────────────────────────────────────────── - // If we can't get size, skip this device (probably not real storage) - // - // PATTERN: let-else for early return/continue on failure - // This is cleaner than nested match statements - let size_path = device_path.join("size"); - let Ok(content) = self.read_sysfs_file(&size_path) else { - continue; - }; - let Ok(size_bytes) = parse_sysfs_size(&content) else { - log::trace!("Cannot parse size for {}: invalid format", device_name); - continue; - }; - - // Skip tiny devices (< 1GB) - probably not real storage - // USB sticks, boot partitions, etc. - const MIN_SIZE: u64 = 1_000_000_000; // 1 GB - if size_bytes < MIN_SIZE { - log::trace!("Skipping small device {}: {} bytes", device_name, size_bytes); - continue; - } - - // ───────────────────────────────────────────────────────────── - // READ ROTATIONAL FLAG - // ───────────────────────────────────────────────────────────── - // 0 = SSD/NVMe (no spinning platters) - // 1 = HDD (spinning platters) - let rotational_path = device_path.join("queue/rotational"); - let is_rotational = self.read_sysfs_file(&rotational_path) - .map(|content| parse_sysfs_rotational(&content)) - .unwrap_or(false); // Default to SSD if unknown - - // ───────────────────────────────────────────────────────────── - // DETERMINE DEVICE TYPE - // ───────────────────────────────────────────────────────────── - // Combines name pattern + rotational flag - let device_type = StorageType::from_device(&device_name, is_rotational); - - // ───────────────────────────────────────────────────────────── - // READ OPTIONAL FIELDS - // ───────────────────────────────────────────────────────────── - // These may fail (especially serial without root) - that's OK - - // Model name - let model = self.read_sysfs_file(&device_path.join("device/model")) - .map(|s| s.trim().to_string()) - .unwrap_or_default(); - - // Serial number (may require root) - let serial_number = self.read_sysfs_file(&device_path.join("device/serial")) - .map(|s| s.trim().to_string()) - .ok() - .filter(|s| !s.is_empty()); - - // Firmware version - let firmware_version = self.read_sysfs_file(&device_path.join("device/firmware_rev")) - .map(|s| s.trim().to_string()) - .ok() - .filter(|s| !s.is_empty()); - - // For NVMe, try alternate paths - let (serial_number, firmware_version) = if device_type == StorageType::Nvme { - self.read_nvme_sysfs_attrs(&device_name, serial_number, firmware_version) - } else { - (serial_number, firmware_version) - }; - - // ───────────────────────────────────────────────────────────── - // DETERMINE INTERFACE TYPE - // ───────────────────────────────────────────────────────────── - let interface = match &device_type { - StorageType::Nvme => "NVMe".to_string(), - StorageType::Emmc => "eMMC".to_string(), - StorageType::Hdd | StorageType::Ssd => "SATA".to_string(), - _ => "Unknown".to_string(), - }; - - // ───────────────────────────────────────────────────────────── - // BUILD THE DEVICE STRUCT - // ───────────────────────────────────────────────────────────── - // PATTERN: Builder pattern - set required fields, then optional - let mut device = StorageDevice { - name: device_name.clone(), - device_path: format!("/dev/{}", device_name), - device_type: device_type.clone(), - type_: device_type.display_name().to_string(), - size_bytes, - model, - serial_number, - firmware_version, - interface, - is_rotational, - detection_method: "sysfs".to_string(), - ..Default::default() - }; - - // Calculate derived fields - device.calculate_size_fields(); - - devices.push(device); - } - - Ok(devices) - } - - /// Read NVMe-specific sysfs attributes. - /// - /// NVMe devices have attributes in a different location: - /// `/sys/class/nvme/nvme0/serial` instead of `/sys/block/nvme0n1/device/serial` - /// - /// # Arguments - /// - /// * `device_name` - Block device name (e.g., "nvme0n1") - /// * `existing_serial` - Serial from block device path (may be None) - /// * `existing_firmware` - Firmware from block device path (may be None) - fn read_nvme_sysfs_attrs( - &self, - device_name: &str, - existing_serial: Option, - existing_firmware: Option, - ) -> (Option, Option) { - // Extract controller name: "nvme0n1" -> "nvme0" - // PATTERN: String manipulation - find pattern and extract - let controller = device_name - .chars() - .take_while(|c| !c.is_ascii_digit() || device_name.starts_with("nvme")) - .take_while(|&c| c != 'n' || device_name.find("nvme").is_some()) - .collect::(); - - // Try to extract just "nvme0" from "nvme0n1" - let controller = if device_name.starts_with("nvme") { - // Find position of 'n' that's followed by a digit (the namespace) - if let Some(pos) = device_name[4..].find('n') { - &device_name[..4 + pos] - } else { - &device_name - } - } else { - &device_name - }; - - let nvme_path = PathBuf::from("/sys/class/nvme").join(controller); - - // Try to get serial from NVMe class path - let serial = existing_serial.or_else(|| { - self.read_sysfs_file(&nvme_path.join("serial")) - .map(|s| s.trim().to_string()) - .ok() - .filter(|s| !s.is_empty()) - }); - - // Try to get firmware from NVMe class path - let firmware = existing_firmware.or_else(|| { - self.read_sysfs_file(&nvme_path.join("firmware_rev")) - .map(|s| s.trim().to_string()) - .ok() - .filter(|s| !s.is_empty()) - }); - - (serial, firmware) - } - - /// Detect storage via lsblk command (JSON output). - /// - /// # Command - /// - /// ```bash - /// lsblk -J -b -o NAME,SIZE,TYPE,MODEL,SERIAL,ROTA,TRAN,WWN - /// ``` - /// - /// # Flags - /// - /// - `-J` = JSON output (easier to parse than text) - /// - `-b` = Size in bytes (not human-readable) - /// - `-o` = Specify columns - /// - /// # When to Use - /// - /// - Enrichment after sysfs (WWN, transport) - /// - Fallback if sysfs fails - async fn detect_storage_lsblk(&self) -> Result, SystemError> { - let cmd = SystemCommand::new("lsblk") - .args(&[ - "-J", // JSON output - "-b", // Bytes (not human readable) - "-d", // No partitions - "-o", "NAME,SIZE,TYPE,MODEL,SERIAL,ROTA,TRAN,WWN", - ]) - .timeout(Duration::from_secs(10)); - - // PATTERN: Combined error mapping with and_then - // Execute command and check success in one chain - let output = self.command_executor.execute(&cmd).await.map_err(|e| { - SystemError::CommandFailed { - command: "lsblk".to_string(), - exit_code: None, - stderr: e.to_string(), - } - })?; - - // PATTERN: Guard clause with let-else for cleaner flow - let output = if output.success { output } else { - return Err(SystemError::CommandFailed { - command: "lsblk".to_string(), - exit_code: output.exit_code, - stderr: output.stderr, - }); - }; - - parse_lsblk_json(&output.stdout).map_err(SystemError::ParseError) - } - - /// Detect storage via sysinfo crate (cross-platform fallback). - /// - /// # Limitations - /// - /// sysinfo provides: - /// - Mounted filesystems (not raw block devices) - /// - Limited metadata (no serial, model, etc.) - /// - /// Use only as last resort. - fn detect_storage_sysinfo(&self) -> Result, SystemError> { - use sysinfo::Disks; - - let disks = Disks::new_with_refreshed_list(); - let mut devices = Vec::new(); - - for disk in disks.iter() { - let size_bytes = disk.total_space(); - - // Skip small devices - if size_bytes < 1_000_000_000 { - continue; - } - - let name = disk.name().to_string_lossy().to_string(); - let name = if name.is_empty() { - disk.mount_point().to_string_lossy().to_string() - } else { - name - }; - - let mut device = StorageDevice { - name, - size_bytes, - detection_method: "sysinfo".to_string(), - ..Default::default() - }; - - device.calculate_size_fields(); - devices.push(device); - } - - Ok(devices) - } - - /// Merge storage info from secondary source into primary. - /// - /// # Strategy - /// - /// 1. Match devices by name - /// 2. Fill in missing fields from secondary - /// 3. Don't overwrite existing data (primary is authoritative) - /// - /// # LeetCode Connection - /// - /// This is the **merge** pattern: - /// - LC #88 Merge Sorted Array - /// - LC #21 Merge Two Sorted Lists - /// - LC #56 Merge Intervals - /// - /// Key insight: we're merging by KEY (device name), not by position. - /// - /// # Complexity - /// - /// Current: O(n × m) where n = primary.len(), m = secondary.len() - /// - /// Could optimize with HashMap for O(n + m), but device lists are small - /// (typically < 20), so linear search is fine and simpler. - fn merge_storage_info( - &self, - primary: &mut Vec, - secondary: Vec, - ) { - for sec_device in secondary { - // PATTERN: if-let-else for merge-or-insert - if let Some(pri_device) = primary.iter_mut().find(|d| d.name == sec_device.name) { - // PATTERN: Option::or() for null coalescing - much cleaner! - // Before: if pri.field.is_none() { pri.field = sec.field; } - // After: pri.field = pri.field.take().or(sec.field); - pri_device.serial_number = pri_device.serial_number.take().or(sec_device.serial_number); - pri_device.firmware_version = pri_device.firmware_version.take().or(sec_device.firmware_version); - pri_device.wwn = pri_device.wwn.take().or(sec_device.wwn); - - // PATTERN: Conditional assignment with && guard - if pri_device.model.is_empty() && !sec_device.model.is_empty() { - pri_device.model = sec_device.model; - } - } else { - // Device not in primary - add it - primary.push(sec_device); - } - } - } -} -``` - ---- - -### Step 3: CPU Detection - -**Location:** Update `get_cpu_info` method (around line 67-102) - -```rust -// ============================================================================= -// CPU DETECTION -// ============================================================================= -// -// ENHANCEMENTS: -// - Add frequency_mhz (numeric, not string) -// - Add cache sizes (L1d, L1i, L2, L3) -// - Add CPU flags/features -// - Better ARM support via /proc/cpuinfo -// -// DETECTION CHAIN: -// 1. sysfs for frequency and cache -// 2. /proc/cpuinfo for model, vendor, flags -// 3. lscpu for topology -// 4. dmidecode for additional data (with privileges) -// ============================================================================= - -async fn get_cpu_info(&self) -> Result { - // Start with basic info from lscpu (existing code) - let lscpu_cmd = SystemCommand::new("lscpu").timeout(Duration::from_secs(10)); - let lscpu_output = self - .command_executor - .execute(&lscpu_cmd) - .await - .map_err(|e| SystemError::CommandFailed { - command: "lscpu".to_string(), - exit_code: None, - stderr: e.to_string(), - })?; - - let mut cpu_info = parse_lscpu_output(&lscpu_output.stdout) - .map_err(SystemError::ParseError)?; - - // ========================================================================= - // ENHANCEMENT 1: Frequency from sysfs - // ========================================================================= - // - // sysfs provides exact frequency in kHz - // More reliable than parsing lscpu string output - // - // Paths: - // - /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq (max frequency) - // - /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq (current) - // ========================================================================= - - // PATTERN: if-let with destructuring for tuple results - // Clean way to handle optional enhancement without nested blocks - if let Ok((freq_mhz, freq_min, freq_max)) = self.detect_cpu_sysfs_frequency().await { - cpu_info.frequency_mhz = freq_mhz; - cpu_info.frequency_min_mhz = freq_min; - cpu_info.frequency_max_mhz = freq_max; - cpu_info.set_speed_string(); - cpu_info.detection_methods.push("sysfs_freq".to_string()); - } - - // ========================================================================= - // ENHANCEMENT 2: Cache from sysfs - // ========================================================================= - // - // sysfs provides detailed cache hierarchy: - // /sys/devices/system/cpu/cpu0/cache/index0/ (L1d typically) - // /sys/devices/system/cpu/cpu0/cache/index1/ (L1i typically) - // /sys/devices/system/cpu/cpu0/cache/index2/ (L2) - // /sys/devices/system/cpu/cpu0/cache/index3/ (L3) - // - // Each has: level, type, size, ways_of_associativity, etc. - // ========================================================================= - - // PATTERN: if-let + for loop with tuple matching - // Avoids deep nesting by using tuple pattern matching - if let Ok(caches) = self.detect_cpu_sysfs_cache().await { - for cache in &caches { - // Tuple matching is cleaner than nested if-else - match (cache.level, cache.cache_type.as_str()) { - (1, "Data") => cpu_info.cache_l1d_kb = Some(cache.size_kb), - (1, "Instruction") => cpu_info.cache_l1i_kb = Some(cache.size_kb), - (2, _) => cpu_info.cache_l2_kb = Some(cache.size_kb), - (3, _) => cpu_info.cache_l3_kb = Some(cache.size_kb), - _ => {} // L4 or unified caches - ignored for now - } - } - cpu_info.caches = caches; - cpu_info.detection_methods.push("sysfs_cache".to_string()); - } - - // ========================================================================= - // ENHANCEMENT 3: Flags and vendor from /proc/cpuinfo - // ========================================================================= - // - // /proc/cpuinfo format differs by architecture: - // - // x86_64: - // vendor_id : GenuineIntel - // flags : fpu vme de pse avx avx2 avx512f ... - // - // aarch64: - // CPU implementer : 0x41 - // CPU part : 0xd0c - // Features : fp asimd evtstrm aes ... - // ========================================================================= - - // PATTERN: if-let with && chaining for conditional field updates - // Each field update only happens if condition is met - if let Ok(proc_info) = self.read_proc_cpuinfo().await { - // PATTERN: Short-circuit with && for conditional assignment - if !proc_info.flags.is_empty() { cpu_info.flags = proc_info.flags; } - - // PATTERN: && chaining avoids nested if blocks - if cpu_info.vendor.is_empty() && !proc_info.vendor.is_empty() { - cpu_info.vendor = proc_info.vendor; - } - - // PATTERN: Option::is_some() then take - or use or_else - cpu_info.microarchitecture = cpu_info.microarchitecture.or(proc_info.microarchitecture); - - cpu_info.detection_methods.push("proc_cpuinfo".to_string()); - } - - // ========================================================================= - // EXISTING: dmidecode enrichment (with privileges) - // ========================================================================= - - let dmidecode_cmd = SystemCommand::new("dmidecode") - .args(&["-t", "processor"]) - .timeout(Duration::from_secs(10)); - - // PATTERN: Nested if-let flattened with && conditions - // Original: if let Ok { if success { if let Ok { ... } } } - // Refactored: Single if-let chain with && guard - if let Ok(output) = self.command_executor.execute_with_privileges(&dmidecode_cmd).await { - if output.success && let Ok(dmidecode_info) = parse_dmidecode_cpu(&output.stdout) { - cpu_info = combine_cpu_info(cpu_info, dmidecode_info); - cpu_info.detection_methods.push("dmidecode".to_string()); - } - } - - // Calculate totals - cpu_info.calculate_totals(); - - // Set architecture - cpu_info.architecture = std::env::consts::ARCH.to_string(); - - Ok(cpu_info) -} -``` - -**Add CPU helper methods:** - -```rust -// ============================================================================= -// CPU HELPER METHODS -// ============================================================================= - -impl LinuxSystemInfoProvider { - /// Detect CPU frequency from sysfs. - /// - /// # sysfs Paths - /// - /// - `/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq` - Max frequency (kHz) - /// - `/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq` - Min frequency (kHz) - /// - `/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq` - Current (kHz) - /// - /// # Returns - /// - /// Tuple of (primary_mhz, min_mhz, max_mhz) - /// - /// # LeetCode Connection - /// - /// File I/O with error handling is like parsing problems: - /// - Handle missing files gracefully - /// - Convert units (kHz → MHz) - async fn detect_cpu_sysfs_frequency(&self) - -> Result<(u32, Option, Option), SystemError> - { - let cpu_path = Path::new("/sys/devices/system/cpu/cpu0/cpufreq"); - - if !cpu_path.exists() { - return Err(SystemError::NotAvailable { - resource: "/sys/devices/system/cpu/cpu0/cpufreq".to_string(), - }); - } - - // Read max frequency (primary) - let max_freq = self.read_sysfs_file(&cpu_path.join("cpuinfo_max_freq")) - .ok() - .and_then(|s| parse_sysfs_freq_khz(&s).ok()); - - // Read min frequency - let min_freq = self.read_sysfs_file(&cpu_path.join("cpuinfo_min_freq")) - .ok() - .and_then(|s| parse_sysfs_freq_khz(&s).ok()); - - // Read current frequency (fallback for primary) - let cur_freq = self.read_sysfs_file(&cpu_path.join("scaling_cur_freq")) - .ok() - .and_then(|s| parse_sysfs_freq_khz(&s).ok()); - - // Use max as primary, fall back to current - let primary = max_freq.or(cur_freq).unwrap_or(0); - - Ok((primary, min_freq, max_freq)) - } - - /// Detect CPU cache hierarchy from sysfs. - /// - /// # sysfs Structure - /// - /// ```text - /// /sys/devices/system/cpu/cpu0/cache/ - /// ├── index0/ # Usually L1 Data - /// │ ├── level # "1" - /// │ ├── type # "Data" - /// │ └── size # "32K" - /// ├── index1/ # Usually L1 Instruction - /// ├── index2/ # Usually L2 - /// └── index3/ # Usually L3 (shared) - /// ``` - /// - /// # LeetCode Connection - /// - /// Directory traversal with structured data extraction: - /// - Similar to LC #102 Level Order Traversal (visiting nodes at each level) - /// - Cache hierarchy IS a tree structure! - async fn detect_cpu_sysfs_cache(&self) -> Result, SystemError> { - let cache_path = Path::new("/sys/devices/system/cpu/cpu0/cache"); - - if !cache_path.exists() { - return Err(SystemError::NotAvailable { - resource: cache_path.to_string_lossy().to_string(), - }); - } - - let mut caches = Vec::new(); - - // Iterate through index0, index1, index2, index3 - for i in 0..10 { - let index_path = cache_path.join(format!("index{}", i)); - - if !index_path.exists() { - break; - } - - // Read cache attributes - let level: u8 = self.read_sysfs_file(&index_path.join("level")) - .ok() - .and_then(|s| s.trim().parse().ok()) - .unwrap_or(0); - - let cache_type = self.read_sysfs_file(&index_path.join("type")) - .map(|s| s.trim().to_string()) - .unwrap_or_else(|_| "Unknown".to_string()); - - let size_kb = self.read_sysfs_file(&index_path.join("size")) - .ok() - .and_then(|s| parse_sysfs_cache_size(&s).ok()) - .unwrap_or(0); - - let ways = self.read_sysfs_file(&index_path.join("ways_of_associativity")) - .ok() - .and_then(|s| s.trim().parse().ok()); - - let line_size = self.read_sysfs_file(&index_path.join("coherency_line_size")) - .ok() - .and_then(|s| s.trim().parse().ok()); - - caches.push(CpuCacheInfo { - level, - cache_type, - size_kb, - ways_of_associativity: ways, - line_size_bytes: line_size, - sets: None, - shared: Some(level >= 3), // L3 is typically shared - }); - } - - Ok(caches) - } - - /// Read and parse /proc/cpuinfo. - /// - /// # Why? - /// - /// /proc/cpuinfo contains: - /// - CPU flags/features (important for workload compatibility) - /// - Vendor identification - /// - ARM CPU part numbers (for microarchitecture detection) - async fn read_proc_cpuinfo(&self) -> Result { - let content = fs::read_to_string("/proc/cpuinfo").map_err(|e| { - SystemError::IoError { - path: "/proc/cpuinfo".to_string(), - message: e.to_string(), - } - })?; - - parse_proc_cpuinfo(&content).map_err(SystemError::ParseError) - } -} -``` - ---- - -### Step 4: GPU Detection - -**Location:** Replace `get_gpu_info` method (around line 172-232) - -```rust -// ============================================================================= -// GPU DETECTION -// ============================================================================= -// -// MULTI-METHOD CHAIN (Chain of Responsibility pattern): -// -// Priority 1: NVML (if feature enabled) -// - Most accurate for NVIDIA -// - Direct library, no parsing -// -// Priority 2: nvidia-smi -// - Fallback for NVIDIA -// - Parse CSV output -// -// Priority 3: rocm-smi -// - AMD GPU detection -// - Parse JSON output -// -// Priority 4: sysfs /sys/class/drm -// - Universal Linux -// - Works for all vendors -// - Limited memory info -// -// Priority 5: lspci -// - Basic enumeration -// - No memory/driver info -// - Last resort -// -// LEETCODE CONNECTION: -// - Chain of Responsibility is like trying multiple approaches -// - LC #322 Coin Change: try different options -// - LC #70 Climbing Stairs: multiple ways to reach goal -// ============================================================================= - -async fn get_gpu_info(&self) -> Result { - let mut devices = Vec::new(); - - // ========================================================================= - // METHOD 1: nvidia-smi (NVIDIA GPUs) - // ========================================================================= - // - // Command: nvidia-smi --query-gpu=... --format=csv,noheader,nounits - // - // Key flags: - // - nounits: Returns "81920" instead of "81920 MiB" - // - noheader: Skip column headers - // - csv: Comma-separated for easy parsing - // - // Fields we query: - // - index, name, uuid, memory.total, memory.free - // - pci.bus_id, driver_version, compute_cap - // ========================================================================= - - if let Ok(nvidia_devices) = self.detect_gpus_nvidia_smi().await { - log::debug!("nvidia-smi detected {} GPUs", nvidia_devices.len()); - devices.extend(nvidia_devices); - } - - // ========================================================================= - // METHOD 2: rocm-smi (AMD GPUs) - // ========================================================================= - // - // Only try if we don't have NVIDIA GPUs (or want both) - // AMD GPUs won't show up via nvidia-smi - // ========================================================================= - - if let Ok(amd_devices) = self.detect_gpus_rocm_smi().await { - log::debug!("rocm-smi detected {} GPUs", amd_devices.len()); - // Merge AMD GPUs (they won't conflict with NVIDIA by name) - devices.extend(amd_devices); - } - - // ========================================================================= - // METHOD 3: sysfs /sys/class/drm (enrichment or fallback) - // ========================================================================= - // - // sysfs provides: - // - PCI vendor/device IDs - // - NUMA node - // - AMD: Memory info via mem_info_vram_total - // - // Use to: - // - Enrich existing devices with NUMA info - // - Fallback detection if commands failed - // ========================================================================= - - if let Ok(drm_devices) = self.detect_gpus_sysfs_drm().await { - log::debug!("sysfs DRM detected {} GPUs", drm_devices.len()); - self.merge_gpu_info(&mut devices, drm_devices); - } - - // ========================================================================= - // METHOD 4: lspci (last resort) - // ========================================================================= - // - // If we still have no GPUs, try lspci - // This only gives us basic enumeration (no memory, driver) - // ========================================================================= - - if devices.is_empty() { - if let Ok(lspci_devices) = self.detect_gpus_lspci().await { - log::debug!("lspci detected {} GPUs", lspci_devices.len()); - devices = lspci_devices; - } - } - - // ========================================================================= - // POST-PROCESSING - // ========================================================================= - - // Re-index devices - for (i, device) in devices.iter_mut().enumerate() { - device.index = i as u32; - - // Ensure legacy memory field is set - #[allow(deprecated)] - if device.memory.is_empty() { - device.set_memory_string(); - } - } - - // Sort by index for consistent output - devices.sort_by_key(|d| d.index); - - log::info!("Detected {} GPUs", devices.len()); - Ok(GpuInfo { devices }) -} -``` - -**Add GPU helper methods:** - -```rust -// ============================================================================= -// GPU HELPER METHODS -// ============================================================================= - -impl LinuxSystemInfoProvider { - /// Detect NVIDIA GPUs via nvidia-smi command. - /// - /// # Command - /// - /// ```bash - /// nvidia-smi --query-gpu=index,name,uuid,memory.total,memory.free,pci.bus_id,driver_version,compute_cap \ - /// --format=csv,noheader,nounits - /// ``` - /// - /// # Output Format - /// - /// ```text - /// 0, NVIDIA H100 80GB HBM3, GPU-xxxx, 81920, 81000, 00000000:01:00.0, 535.129.03, 9.0 - /// ``` - /// - /// # LeetCode Connection - /// - /// CSV parsing is like string manipulation problems: - /// - LC #68 Text Justification - /// - LC #722 Remove Comments - async fn detect_gpus_nvidia_smi(&self) -> Result, SystemError> { - let cmd = SystemCommand::new("nvidia-smi") - .args(&[ - "--query-gpu=index,name,uuid,memory.total,memory.free,pci.bus_id,driver_version,compute_cap", - "--format=csv,noheader,nounits", - ]) - .timeout(Duration::from_secs(10)); - - let output = self.command_executor.execute(&cmd).await.map_err(|e| { - SystemError::CommandFailed { - command: "nvidia-smi".to_string(), - exit_code: None, - stderr: e.to_string(), - } - })?; - - // PATTERN: let-else for guard clause - // Cleaner than if !success { return Err } - let output = if output.success { output } else { - return Err(SystemError::CommandFailed { - command: "nvidia-smi".to_string(), - exit_code: output.exit_code, - stderr: output.stderr, - }); - }; - - parse_nvidia_smi_output(&output.stdout).map_err(SystemError::ParseError) - } - - /// Detect AMD GPUs via rocm-smi command. - /// - /// # Command - /// - /// ```bash - /// rocm-smi --showproductname --showmeminfo vram --showdriver --json - /// ``` - async fn detect_gpus_rocm_smi(&self) -> Result, SystemError> { - let cmd = SystemCommand::new("rocm-smi") - .args(&["--showproductname", "--showmeminfo", "vram", "--showdriver", "--json"]) - .timeout(Duration::from_secs(10)); - - // PATTERN: Match with guard clause for conditional success - // This is idiomatic when you need both Ok AND a condition - let Ok(output) = self.command_executor.execute(&cmd).await else { - return Ok(Vec::new()); // rocm-smi not available - not an AMD system - }; - if !output.success { - return Ok(Vec::new()); // Command failed - not an AMD system - } - - // Parse rocm-smi JSON output - // TODO: Implement parse_rocm_smi_output in parsers/gpu.rs - self.parse_rocm_smi_json(&output.stdout) - } - - /// Parse rocm-smi JSON output (inline for now). - fn parse_rocm_smi_json(&self, output: &str) -> Result, SystemError> { - let json: serde_json::Value = serde_json::from_str(output) - .map_err(|e| SystemError::ParseError(format!("rocm-smi JSON parse error: {}", e)))?; - - // PATTERN: let-else for early return when required data missing - let Some(obj) = json.as_object() else { - return Ok(Vec::new()); // Not a JSON object - no GPUs - }; - - // PATTERN: filter_map + enumerate for index tracking - // Cleaner than manual index increment - let devices: Vec = obj.iter() - .filter(|(key, _)| key.starts_with("card")) - .enumerate() - .map(|(index, (_, value))| { - // PATTERN: and_then chains for nested Option extraction - let name = value.get("Card series") - .and_then(|v| v.as_str()) - .unwrap_or("AMD GPU") - .to_string(); - - let memory_bytes = value.get("VRAM Total Memory (B)") - .and_then(|v| v.as_str()) - .and_then(|s| s.parse::().ok()) - .unwrap_or(0); - - let driver_version = value.get("Driver version") - .and_then(|v| v.as_str()) - .map(String::from); - - GpuDevice { - index: index as u32, - name, - uuid: format!("amd-gpu-{}", index), - memory_total_mb: memory_bytes / (1024 * 1024), - driver_version, - vendor: GpuVendor::Amd, - vendor_name: "AMD".to_string(), - detection_method: "rocm-smi".to_string(), - ..Default::default() - } - }) - .collect(); - - Ok(devices) - } - - /// Detect GPUs via sysfs DRM interface. - /// - /// # sysfs Paths - /// - /// ```text - /// /sys/class/drm/card0/device/ - /// ├── vendor # PCI vendor ID ("0x10de" = NVIDIA) - /// ├── device # PCI device ID - /// ├── numa_node # NUMA affinity - /// └── mem_info_vram_total # AMD: VRAM size in bytes - /// ``` - /// - /// # Use Cases - /// - /// - Get NUMA node info for all GPUs - /// - Get memory info for AMD GPUs - /// - Fallback enumeration - async fn detect_gpus_sysfs_drm(&self) -> Result, SystemError> { - let drm_path = Path::new("/sys/class/drm"); - - if !drm_path.exists() { - return Err(SystemError::NotAvailable { - resource: "/sys/class/drm".to_string(), - }); - } - - let mut devices = Vec::new(); - let mut index = 0; - - let entries = fs::read_dir(drm_path).map_err(|e| SystemError::IoError { - path: "/sys/class/drm".to_string(), - message: e.to_string(), - })?; - - for entry in entries.flatten() { - let name = entry.file_name().to_string_lossy().to_string(); - - // Only process card* entries (not renderD*) - if !name.starts_with("card") || name.contains("-") { - continue; - } - - let card_path = entry.path(); - let device_path = card_path.join("device"); - - if !device_path.exists() { - continue; - } - - // Read PCI vendor ID - let vendor_id = self.read_sysfs_file(&device_path.join("vendor")) - .map(|s| s.trim().trim_start_matches("0x").to_string()) - .unwrap_or_default(); - - let vendor = GpuVendor::from_pci_vendor(&vendor_id); - - // Skip if not a GPU vendor we recognize - if vendor == GpuVendor::Unknown { - continue; - } - - // Read PCI device ID - let device_id = self.read_sysfs_file(&device_path.join("device")) - .map(|s| s.trim().trim_start_matches("0x").to_string()) - .unwrap_or_default(); - - // Read NUMA node - let numa_node = self.read_sysfs_file(&device_path.join("numa_node")) - .ok() - .and_then(|s| s.trim().parse::().ok()); - - // AMD-specific: Read VRAM size - let memory_total_mb = if vendor == GpuVendor::Amd { - self.read_sysfs_file(&device_path.join("mem_info_vram_total")) - .ok() - .and_then(|s| s.trim().parse::().ok()) - .map(|bytes| bytes / (1024 * 1024)) - .unwrap_or(0) - } else { - 0 - }; - - let device = GpuDevice { - index, - name: format!("{} GPU ({})", vendor.name(), name), - uuid: format!("drm-{}", name), - memory_total_mb, - pci_id: format!("{}:{}", vendor_id, device_id), - vendor: vendor.clone(), - vendor_name: vendor.name().to_string(), - numa_node, - detection_method: "sysfs".to_string(), - ..Default::default() - }; - - devices.push(device); - index += 1; - } - - Ok(devices) - } - - /// Detect GPUs via lspci command (last resort). - async fn detect_gpus_lspci(&self) -> Result, SystemError> { - let cmd = SystemCommand::new("lspci") - .args(&["-nn"]) - .timeout(Duration::from_secs(5)); - - let output = self.command_executor.execute(&cmd).await.map_err(|e| { - SystemError::CommandFailed { - command: "lspci".to_string(), - exit_code: None, - stderr: e.to_string(), - } - })?; - - if !output.success { - return Err(SystemError::CommandFailed { - command: "lspci".to_string(), - exit_code: output.exit_code, - stderr: output.stderr.clone(), - }); - } - - parse_lspci_gpu_output(&output.stdout).map_err(SystemError::ParseError) - } - - /// Merge GPU info from secondary source into primary. - /// - /// Match by PCI bus ID or name, fill in missing fields. - fn merge_gpu_info(&self, primary: &mut Vec, secondary: Vec) { - for sec_gpu in secondary { - // PATTERN: and_then + find with predicate chaining - // Cleaner than nested if-let - let matched = sec_gpu.pci_bus_id.as_ref().and_then(|sec_bus_id| { - primary.iter_mut().find(|g| { - g.pci_bus_id.as_ref().is_some_and(|id| id == sec_bus_id) - }) - }); - - // PATTERN: if-let-else for merge-or-insert logic - if let Some(pri_gpu) = matched { - // PATTERN: or/or_else for null coalescing - pri_gpu.numa_node = pri_gpu.numa_node.or(sec_gpu.numa_node); - if pri_gpu.pci_id.is_empty() { pri_gpu.pci_id = sec_gpu.pci_id; } - if pri_gpu.memory_total_mb == 0 { pri_gpu.memory_total_mb = sec_gpu.memory_total_mb; } - } else if !primary.iter().any(|g| g.name == sec_gpu.name) { - primary.push(sec_gpu); - } - } - } -} -``` - ---- - -### Step 5: Network Detection - -**Location:** Update `get_network_info` method (around line 234-252) - -```rust -// ============================================================================= -// NETWORK DETECTION -// ============================================================================= -// -// ENHANCEMENTS: -// - Add driver and driver_version -// - Add MTU -// - Add is_up, is_virtual -// - Add speed_mbps (numeric) -// ============================================================================= - -async fn get_network_info(&self) -> Result { - // Get basic interface info from ip command (existing code) - let ip_cmd = SystemCommand::new("ip") - .args(&["addr", "show"]) - .timeout(Duration::from_secs(5)); - - let ip_output = self.command_executor.execute(&ip_cmd).await.map_err(|e| { - SystemError::CommandFailed { - command: "ip".to_string(), - exit_code: None, - stderr: e.to_string(), - } - })?; - - let mut interfaces = parse_ip_output(&ip_output.stdout) - .map_err(SystemError::ParseError)?; - - // ========================================================================= - // ENHANCEMENT: Enrich with sysfs data - // ========================================================================= - // - // sysfs provides: - // - /sys/class/net/{iface}/operstate (up/down) - // - /sys/class/net/{iface}/speed (Mbps, may be -1) - // - /sys/class/net/{iface}/mtu - // - /sys/class/net/{iface}/device/driver -> symlink to driver - // ========================================================================= - - for iface in &mut interfaces { - self.enrich_network_interface_sysfs(iface).await; - } - - Ok(NetworkInfo { - interfaces, - infiniband: None, // TODO: Add infiniband detection - }) -} -``` - -**Add network helper methods:** - -```rust -// ============================================================================= -// NETWORK HELPER METHODS -// ============================================================================= - -impl LinuxSystemInfoProvider { - /// Enrich network interface with sysfs data. - /// - /// # sysfs Paths - /// - /// ```text - /// /sys/class/net/{iface}/ - /// ├── operstate # "up", "down", "unknown" - /// ├── speed # Mbps (may be -1 if unknown) - /// ├── mtu # MTU in bytes - /// ├── carrier # 1 = link detected - /// └── device/ - /// └── driver/ # Symlink to driver module - /// └── module/ - /// └── version # Driver version - /// ``` - async fn enrich_network_interface_sysfs(&self, iface: &mut NetworkInterface) { - let iface_path = PathBuf::from("/sys/class/net").join(&iface.name); - - if !iface_path.exists() { - return; - } - - // ───────────────────────────────────────────────────────────── - // OPERATIONAL STATE - // ───────────────────────────────────────────────────────────── - iface.is_up = self.read_sysfs_file(&iface_path.join("operstate")) - .map(|s| s.trim().to_lowercase() == "up") - .unwrap_or(false); - - // ───────────────────────────────────────────────────────────── - // SPEED - // ───────────────────────────────────────────────────────────── - // May be -1 if link is down or speed is unknown - // PATTERN: if-let chain with && for multiple conditions - if let Ok(speed_str) = self.read_sysfs_file(&iface_path.join("speed")) - && let Ok(speed) = speed_str.trim().parse::() - && speed > 0 - { - iface.speed_mbps = Some(speed as u32); - iface.speed = Some(format!("{} Mbps", speed)); - } - - // ───────────────────────────────────────────────────────────── - // MTU - // ───────────────────────────────────────────────────────────── - // PATTERN: if-let chain - parse only if read succeeds - if let Ok(mtu_str) = self.read_sysfs_file(&iface_path.join("mtu")) - && let Ok(mtu) = mtu_str.trim().parse::() - { - iface.mtu = mtu; - } - - // ───────────────────────────────────────────────────────────── - // CARRIER (link detected) - // ───────────────────────────────────────────────────────────── - iface.carrier = self.read_sysfs_file(&iface_path.join("carrier")) - .map(|s| s.trim() == "1") - .ok(); - - // ───────────────────────────────────────────────────────────── - // VIRTUAL INTERFACE DETECTION - // ───────────────────────────────────────────────────────────── - // Virtual interfaces don't have a physical device - let device_path = iface_path.join("device"); - iface.is_virtual = !device_path.exists() - || iface.name.starts_with("lo") - || iface.name.starts_with("veth") - || iface.name.starts_with("br") - || iface.name.starts_with("docker") - || iface.name.starts_with("virbr"); - - // ───────────────────────────────────────────────────────────── - // DRIVER INFORMATION (only for physical interfaces) - // ───────────────────────────────────────────────────────────── - // PATTERN: Negated guard with if-let chain - // Only process driver info for non-virtual interfaces - if !iface.is_virtual { - let driver_link = device_path.join("driver"); - // PATTERN: if-let chain with && for nested conditionals - if let Ok(driver_path) = fs::read_link(&driver_link) - && let Some(driver_name) = driver_path.file_name() - { - let driver_str = driver_name.to_string_lossy().to_string(); - iface.driver = Some(driver_str.clone()); - - // Chain: driver version lookup - let version_path = PathBuf::from("/sys/module").join(&driver_str).join("version"); - if let Ok(version) = self.read_sysfs_file(&version_path) { - iface.driver_version = Some(version.trim().to_string()); - } - } - } - - // ───────────────────────────────────────────────────────────── - // INTERFACE TYPE - // ───────────────────────────────────────────────────────────── - // PATTERN: Match-like if-else chain for classification - iface.interface_type = if iface.name == "lo" { - NetworkInterfaceType::Loopback - } else if iface.name.starts_with("br") || iface.name.starts_with("virbr") { - NetworkInterfaceType::Bridge - } else if iface.name.starts_with("veth") { - NetworkInterfaceType::Veth - } else if iface.name.starts_with("bond") { - NetworkInterfaceType::Bond - } else if iface.name.contains(".") { - NetworkInterfaceType::Vlan - } else if iface.name.starts_with("wl") { - NetworkInterfaceType::Wireless - } else if iface.name.starts_with("ib") { - NetworkInterfaceType::Infiniband - } else { - NetworkInterfaceType::Ethernet - }; - } -} -``` - ---- - -## Helper Functions - -**Add this general-purpose helper at the end of the impl block:** - -```rust -// ============================================================================= -// GENERAL HELPER METHODS -// ============================================================================= - -impl LinuxSystemInfoProvider { - /// Read a sysfs file and return contents as String. - /// - /// # Error Handling - /// - /// Returns Err if: - /// - File doesn't exist - /// - Permission denied - /// - Any I/O error - /// - /// # Why This Helper? - /// - /// - Centralizes error handling - /// - Consistent logging - /// - Can add caching later if needed - fn read_sysfs_file(&self, path: &Path) -> Result { - fs::read_to_string(path) - } -} -``` - ---- - -## Testing - -After implementing, verify with: - -```bash -# Check compilation -cargo check - -# Run tests -cargo test - -# Test on real hardware (run binary) -cargo run --bin hardware_report - -# Check specific detection -cargo run --bin hardware_report 2>&1 | grep -A 5 "storage" -cargo run --bin hardware_report 2>&1 | grep -A 5 "gpus" -``` - ---- - -## Rust Idioms: if-let Chaining & let-else - -This implementation uses modern Rust patterns to reduce nesting: - -### let-else (Early Exit Pattern) - -```rust -// BEFORE: Nested match/if for required values -let size_bytes = match self.read_sysfs_file(&path) { - Ok(content) => match parse_sysfs_size(&content) { - Ok(size) => size, - Err(_) => continue, - }, - Err(_) => continue, -}; - -// AFTER: let-else for early exit -let Ok(content) = self.read_sysfs_file(&path) else { continue; }; -let Ok(size_bytes) = parse_sysfs_size(&content) else { continue; }; -``` - -### if-let Chaining with && - -```rust -// BEFORE: Nested if-let -if let Ok(speed_str) = read_file(&path) { - if let Ok(speed) = speed_str.parse::() { - if speed > 0 { - iface.speed = Some(speed); - } - } -} - -// AFTER: Chained if-let with && -if let Ok(speed_str) = read_file(&path) - && let Ok(speed) = speed_str.parse::() - && speed > 0 -{ - iface.speed = Some(speed); -} -``` - -### Option::or() for Null Coalescing - -```rust -// BEFORE: Verbose conditional assignment -if pri.serial.is_none() { - pri.serial = sec.serial; -} - -// AFTER: Functional style -pri.serial = pri.serial.take().or(sec.serial); -``` - -### and_then Chains - -```rust -// BEFORE: Nested if-let for Option extraction -if let Some(bus_id) = &sec_gpu.pci_bus_id { - if let Some(pri) = primary.iter_mut().find(|g| ...) { - // merge - } -} - -// AFTER: and_then chain -let matched = sec_gpu.pci_bus_id.as_ref().and_then(|bus_id| { - primary.iter_mut().find(|g| g.pci_bus_id.as_ref().is_some_and(|id| id == bus_id)) -}); -``` - ---- - -## LeetCode Pattern Summary - -| Pattern | Problems | Where Used | -|---------|----------|------------| -| **Chain of Responsibility** | - | All detection methods (sysfs → command → fallback) | -| **Merge/Combine** | LC #88, #21, #56 | `merge_storage_info`, `merge_gpu_info` | -| **Tree Traversal** | LC #102, #200 | sysfs directory walking, cache hierarchy | -| **Filtering** | LC #283, #27 | `devices.retain()` for virtual devices | -| **Hash Map Lookup** | LC #1, #49 | Vendor ID → vendor name | -| **String Parsing** | LC #8, #65 | sysfs file parsing | -| **Pattern Matching** | LC #28, #10 | lspci PCI ID extraction | -| **Two Pointers** | LC #88 | Merge operations | - ---- - -## Implementation Checklist - -Use this to track your progress: - -```markdown -## Storage Detection -- [ ] Update imports -- [ ] Replace get_storage_info method -- [ ] Add detect_storage_sysfs helper -- [ ] Add detect_storage_lsblk helper -- [ ] Add detect_storage_sysinfo helper -- [ ] Add read_nvme_sysfs_attrs helper -- [ ] Add merge_storage_info helper -- [ ] Test: cargo check - -## CPU Detection -- [ ] Update get_cpu_info method -- [ ] Add detect_cpu_sysfs_frequency helper -- [ ] Add detect_cpu_sysfs_cache helper -- [ ] Add read_proc_cpuinfo helper -- [ ] Test: cargo check - -## GPU Detection -- [ ] Replace get_gpu_info method -- [ ] Add detect_gpus_nvidia_smi helper -- [ ] Add detect_gpus_rocm_smi helper -- [ ] Add parse_rocm_smi_json helper -- [ ] Add detect_gpus_sysfs_drm helper -- [ ] Add detect_gpus_lspci helper -- [ ] Add merge_gpu_info helper -- [ ] Test: cargo check - -## Network Detection -- [ ] Update get_network_info method -- [ ] Add enrich_network_interface_sysfs helper -- [ ] Test: cargo check - -## Final -- [ ] Add read_sysfs_file general helper -- [ ] Run full test suite: cargo test -- [ ] Test on real hardware -``` - -Good luck with your implementation! diff --git a/docs/MEMORY_ENHANCEMENTS.md b/docs/MEMORY_ENHANCEMENTS.md deleted file mode 100644 index a2e0396..0000000 --- a/docs/MEMORY_ENHANCEMENTS.md +++ /dev/null @@ -1,662 +0,0 @@ -# Memory Enhancement Plan - -> **Category:** Data Gap -> **Target Platforms:** Linux (x86_64, aarch64) -> **Priority:** Medium - Missing DIMM part_number and numeric fields - -## Table of Contents - -1. [Problem Statement](#problem-statement) -2. [Current Implementation](#current-implementation) -3. [Entity Changes](#entity-changes) -4. [Detection Method Details](#detection-method-details) -5. [Adapter Implementation](#adapter-implementation) -6. [Parser Implementation](#parser-implementation) -7. [Testing Requirements](#testing-requirements) -8. [References](#references) - ---- - -## Problem Statement - -### Current Issue - -The `MemoryModule` structure lacks the `part_number` field and uses string-based sizes: - -```rust -// Current struct - missing fields -pub struct MemoryModule { - pub size: String, // String, not numeric - pub type_: String, - pub speed: String, // String, not numeric - pub location: String, - pub manufacturer: String, - pub serial: String, - // Missing: part_number, rank, configured_voltage -} -``` - -### Impact - -- Cannot track memory part numbers for procurement/warranty -- Size as string breaks capacity calculations -- No memory rank information for performance analysis -- Missing voltage data for power analysis - -### Requirements - -1. **Add part_number field** - For asset tracking -2. **Numeric size field** - `size_bytes: u64` -3. **Numeric speed field** - `speed_mhz: Option` -4. **Additional metadata** - rank, voltage, bank locator - ---- - -## Current Implementation - -### Location - -- **Entity:** `src/domain/entities.rs:178-205` -- **Adapter:** `src/adapters/secondary/system/linux.rs:104-151` -- **Parser:** `src/domain/parsers/memory.rs` - -### Current Detection Flow - -``` -┌──────────────────────────────────────────┐ -│ LinuxSystemInfoProvider::get_memory_info()│ -└──────────────────────────────────────────┘ - │ - ▼ - ┌──────────────────┐ - │ free -b │──────▶ Total memory - └──────────────────┘ - │ - ▼ - ┌───────────────────────┐ - │ dmidecode -t memory │──────▶ Module details - │ (requires privileges) │ - └───────────────────────┘ -``` - ---- - -## Entity Changes - -### New MemoryModule Structure - -```rust -// src/domain/entities.rs - -/// Memory technology type -/// -/// # References -/// -/// - [JEDEC Standards](https://www.jedec.org/) -/// - [SMBIOS Type 17](https://www.dmtf.org/standards/smbios) -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -pub enum MemoryType { - /// DDR4 SDRAM - Ddr4, - /// DDR5 SDRAM - Ddr5, - /// LPDDR4 (Low Power DDR4) - Lpddr4, - /// LPDDR5 (Low Power DDR5) - Lpddr5, - /// DDR3 SDRAM - Ddr3, - /// HBM (High Bandwidth Memory) - Hbm, - /// HBM2 - Hbm2, - /// HBM3 - Hbm3, - /// Unknown type - Unknown, -} - -impl MemoryType { - /// Parse memory type from SMBIOS/dmidecode string - /// - /// # Arguments - /// - /// * `type_str` - Type string from dmidecode (e.g., "DDR4", "DDR5") - /// - /// # Example - /// - /// ``` - /// use hardware_report::MemoryType; - /// - /// assert_eq!(MemoryType::from_string("DDR4"), MemoryType::Ddr4); - /// assert_eq!(MemoryType::from_string("LPDDR5"), MemoryType::Lpddr5); - /// ``` - pub fn from_string(type_str: &str) -> Self { - match type_str.to_uppercase().as_str() { - "DDR4" => MemoryType::Ddr4, - "DDR5" => MemoryType::Ddr5, - "LPDDR4" | "LPDDR4X" => MemoryType::Lpddr4, - "LPDDR5" | "LPDDR5X" => MemoryType::Lpddr5, - "DDR3" => MemoryType::Ddr3, - "HBM" => MemoryType::Hbm, - "HBM2" | "HBM2E" => MemoryType::Hbm2, - "HBM3" | "HBM3E" => MemoryType::Hbm3, - _ => MemoryType::Unknown, - } - } -} - -/// Memory form factor -/// -/// # References -/// -/// - [SMBIOS Type 17 Form Factor](https://www.dmtf.org/standards/smbios) -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -pub enum MemoryFormFactor { - /// Standard DIMM - Dimm, - /// Small Outline DIMM (laptops) - SoDimm, - /// Registered DIMM (servers) - Rdimm, - /// Load Reduced DIMM (servers) - Lrdimm, - /// Unbuffered DIMM - Udimm, - /// Non-volatile DIMM - Nvdimm, - /// High Bandwidth Memory - Hbm, - /// Unknown form factor - Unknown, -} - -/// Individual memory module (DIMM) information -/// -/// Represents a single memory module with comprehensive metadata -/// for CMDB inventory and capacity planning. -/// -/// # Detection Methods -/// -/// Memory module information is gathered from: -/// 1. **dmidecode -t memory** - SMBIOS Type 17 data (requires privileges) -/// 2. **sysfs /sys/devices/system/memory** - Basic memory info -/// 3. **sysinfo crate** - Total memory fallback -/// -/// # Part Number -/// -/// The `part_number` field contains the manufacturer's part number, -/// which is essential for: -/// - Procurement and ordering replacements -/// - Warranty tracking -/// - Compatibility verification -/// -/// # Example -/// -/// ``` -/// use hardware_report::MemoryModule; -/// -/// // Calculate total memory from modules -/// let total_gb: f64 = modules.iter() -/// .map(|m| m.size_bytes as f64) -/// .sum::() / (1024.0 * 1024.0 * 1024.0); -/// ``` -/// -/// # References -/// -/// - [JEDEC Memory Standards](https://www.jedec.org/) -/// - [SMBIOS Specification](https://www.dmtf.org/standards/smbios) -/// - [dmidecode](https://www.nongnu.org/dmidecode/) -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct MemoryModule { - /// Physical slot/bank locator (e.g., "DIMM_A1", "ChannelA-DIMM0") - /// - /// From SMBIOS "Locator" field. - pub location: String, - - /// Bank locator (e.g., "BANK 0", "P0 CHANNEL A") - /// - /// From SMBIOS "Bank Locator" field. - pub bank_locator: Option, - - /// Module size in bytes - /// - /// Primary size field for calculations. - pub size_bytes: u64, - - /// Module size as human-readable string (e.g., "32 GB") - /// - /// Convenience field for display. - pub size: String, - - /// Memory technology type - pub memory_type: MemoryType, - - /// Memory type as string (e.g., "DDR4", "DDR5") - /// - /// For backward compatibility. - pub type_: String, - - /// Memory speed in MT/s (megatransfers per second) - /// - /// This is the data rate, not the clock frequency. - /// DDR4-3200 = 3200 MT/s = 1600 MHz clock. - pub speed_mts: Option, - - /// Configured clock speed in MHz - pub speed_mhz: Option, - - /// Speed as string (e.g., "3200 MT/s") - /// - /// For backward compatibility. - pub speed: String, - - /// Form factor - pub form_factor: MemoryFormFactor, - - /// Manufacturer name (e.g., "Samsung", "Micron", "SK Hynix") - pub manufacturer: String, - - /// Module serial number - pub serial: String, - - /// Manufacturer part number - /// - /// Essential for procurement and warranty tracking. - /// - /// # Example - /// - /// - "M393A4K40EB3-CWE" (Samsung 32GB DDR4-3200) - /// - "MTA36ASF8G72PZ-3G2E1" (Micron 64GB DDR4-3200) - pub part_number: Option, - - /// Number of memory ranks - /// - /// Single rank (1R), Dual rank (2R), Quad rank (4R), Octal rank (8R). - /// Higher rank counts can affect performance and compatibility. - pub rank: Option, - - /// Data width in bits (e.g., 64, 72 for ECC) - pub data_width_bits: Option, - - /// Total width in bits (includes ECC bits if present) - pub total_width_bits: Option, - - /// Whether ECC (Error Correcting Code) is supported - pub ecc: Option, - - /// Configured voltage in volts (e.g., 1.2, 1.35) - pub voltage: Option, - - /// Minimum voltage in volts - pub voltage_min: Option, - - /// Maximum voltage in volts - pub voltage_max: Option, - - /// Asset tag (if set by administrator) - pub asset_tag: Option, -} - -impl Default for MemoryModule { - fn default() -> Self { - Self { - location: String::new(), - bank_locator: None, - size_bytes: 0, - size: String::new(), - memory_type: MemoryType::Unknown, - type_: String::new(), - speed_mts: None, - speed_mhz: None, - speed: String::new(), - form_factor: MemoryFormFactor::Unknown, - manufacturer: String::new(), - serial: String::new(), - part_number: None, - rank: None, - data_width_bits: None, - total_width_bits: None, - ecc: None, - voltage: None, - voltage_min: None, - voltage_max: None, - asset_tag: None, - } - } -} - -/// System memory information -/// -/// Container for overall memory statistics and individual modules. -/// -/// # References -/// -/// - [/proc/meminfo](https://man7.org/linux/man-pages/man5/proc.5.html) -/// - [SMBIOS Type 16 & 17](https://www.dmtf.org/standards/smbios) -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct MemoryInfo { - /// Total system memory in bytes - pub total_bytes: u64, - - /// Total memory as human-readable string (e.g., "256 GB") - pub total: String, - - /// Primary memory type across all modules - pub type_: String, - - /// Primary memory speed - pub speed: String, - - /// Number of populated DIMM slots - pub populated_slots: u32, - - /// Total number of DIMM slots - pub total_slots: Option, - - /// Maximum supported memory capacity in bytes - pub max_capacity_bytes: Option, - - /// Whether ECC is enabled system-wide - pub ecc_enabled: Option, - - /// Individual memory modules - pub modules: Vec, -} -``` - ---- - -## Detection Method Details - -### Method 1: dmidecode -t memory - -**When:** Linux with privileges (primary source) - -**Command:** -```bash -dmidecode -t 17 # Memory Device (each DIMM) -dmidecode -t 16 # Physical Memory Array (capacity/slots) -``` - -**SMBIOS Type 17 Fields:** - -| Field | Description | Maps To | -|-------|-------------|---------| -| Size | Module size | `size_bytes`, `size` | -| Locator | Slot name | `location` | -| Bank Locator | Bank name | `bank_locator` | -| Type | DDR4, DDR5, etc. | `type_`, `memory_type` | -| Speed | MT/s rating | `speed_mts`, `speed` | -| Configured Memory Speed | Actual MHz | `speed_mhz` | -| Manufacturer | OEM name | `manufacturer` | -| Serial Number | Serial | `serial` | -| Part Number | OEM part# | `part_number` | -| Rank | 1, 2, 4, 8 | `rank` | -| Configured Voltage | Volts | `voltage` | -| Form Factor | DIMM, SODIMM | `form_factor` | -| Data Width | 64, 72 bits | `data_width_bits` | -| Total Width | 64, 72 bits | `total_width_bits` | - -**Example dmidecode output:** -``` -Memory Device - Size: 32 GB - Locator: DIMM_A1 - Bank Locator: BANK 0 - Type: DDR4 - Speed: 3200 MT/s - Manufacturer: Samsung - Serial Number: 12345678 - Part Number: M393A4K40EB3-CWE - Rank: 2 - Configured Memory Speed: 3200 MT/s - Configured Voltage: 1.2 V -``` - -**References:** -- [dmidecode](https://www.nongnu.org/dmidecode/) -- [SMBIOS Specification](https://www.dmtf.org/standards/smbios) - ---- - -### Method 2: sysfs /sys/devices/system/memory - -**When:** Basic memory info without privileges - -**Paths:** -``` -/sys/devices/system/memory/ -├── block_size_bytes # Memory block size -├── memory0/ # First memory block -│ ├── online # 1 if online -│ ├── state # online/offline -│ └── phys_index # Physical address -└── ... -``` - -**Limitation:** Does not provide DIMM-level details like part number. - ---- - -### Method 3: /proc/meminfo - -**When:** Total memory fallback - -**Path:** `/proc/meminfo` - -**Format:** -``` -MemTotal: 263736560 kB -MemFree: 8472348 kB -MemAvailable: 245678912 kB -... -``` - ---- - -## Parser Implementation - -### File: `src/domain/parsers/memory.rs` - -```rust -//! Memory information parsing functions -//! -//! This module provides pure parsing functions for memory information from -//! various sources, primarily dmidecode output. -//! -//! # References -//! -//! - [SMBIOS Specification](https://www.dmtf.org/standards/smbios) -//! - [dmidecode](https://www.nongnu.org/dmidecode/) - -use crate::domain::{MemoryFormFactor, MemoryInfo, MemoryModule, MemoryType}; - -/// Parse dmidecode Type 17 (Memory Device) output -/// -/// # Arguments -/// -/// * `output` - Output from `dmidecode -t 17` -/// -/// # Returns -/// -/// Vector of memory modules with all available fields populated. -/// -/// # Example -/// -/// ``` -/// use hardware_report::domain::parsers::memory::parse_dmidecode_memory_device; -/// -/// let output = r#" -/// Memory Device -/// Size: 32 GB -/// Locator: DIMM_A1 -/// Part Number: M393A4K40EB3-CWE -/// "#; -/// -/// let modules = parse_dmidecode_memory_device(output).unwrap(); -/// assert_eq!(modules[0].part_number, Some("M393A4K40EB3-CWE".to_string())); -/// ``` -/// -/// # References -/// -/// - [SMBIOS Type 17](https://www.dmtf.org/standards/smbios) -pub fn parse_dmidecode_memory_device(output: &str) -> Result, String> { - todo!() -} - -/// Parse memory size string to bytes -/// -/// # Arguments -/// -/// * `size_str` - Size string (e.g., "32 GB", "16384 MB", "No Module Installed") -/// -/// # Returns -/// -/// Size in bytes, or 0 if not installed/unknown. -/// -/// # Example -/// -/// ``` -/// use hardware_report::domain::parsers::memory::parse_memory_size; -/// -/// assert_eq!(parse_memory_size("32 GB"), 32 * 1024 * 1024 * 1024); -/// assert_eq!(parse_memory_size("16384 MB"), 16384 * 1024 * 1024); -/// assert_eq!(parse_memory_size("No Module Installed"), 0); -/// ``` -pub fn parse_memory_size(size_str: &str) -> u64 { - let s = size_str.trim(); - - if s.contains("No Module") || s.contains("Unknown") || s.contains("Not Installed") { - return 0; - } - - let parts: Vec<&str> = s.split_whitespace().collect(); - if parts.len() < 2 { - return 0; - } - - let value: u64 = match parts[0].parse() { - Ok(v) => v, - Err(_) => return 0, - }; - - match parts[1].to_uppercase().as_str() { - "GB" => value * 1024 * 1024 * 1024, - "MB" => value * 1024 * 1024, - "KB" => value * 1024, - _ => 0, - } -} - -/// Parse memory speed to MT/s -/// -/// # Arguments -/// -/// * `speed_str` - Speed string (e.g., "3200 MT/s", "2666 MHz") -/// -/// # Returns -/// -/// Speed in MT/s. -pub fn parse_memory_speed(speed_str: &str) -> Option { - let s = speed_str.trim(); - - if s.contains("Unknown") { - return None; - } - - let parts: Vec<&str> = s.split_whitespace().collect(); - if parts.is_empty() { - return None; - } - - parts[0].parse().ok() -} - -/// Parse /proc/meminfo for total memory -/// -/// # Arguments -/// -/// * `content` - Content of /proc/meminfo -/// -/// # Returns -/// -/// Total memory in bytes. -/// -/// # References -/// -/// - [/proc/meminfo](https://man7.org/linux/man-pages/man5/proc.5.html) -pub fn parse_proc_meminfo(content: &str) -> Result { - for line in content.lines() { - if line.starts_with("MemTotal:") { - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() >= 2 { - if let Ok(kb) = parts[1].parse::() { - return Ok(kb * 1024); // Convert KB to bytes - } - } - } - } - Err("MemTotal not found in /proc/meminfo".to_string()) -} - -/// Parse form factor string to enum -/// -/// # Arguments -/// -/// * `ff_str` - Form factor from dmidecode (e.g., "DIMM", "SODIMM") -pub fn parse_form_factor(ff_str: &str) -> MemoryFormFactor { - match ff_str.trim().to_uppercase().as_str() { - "DIMM" => MemoryFormFactor::Dimm, - "SODIMM" | "SO-DIMM" => MemoryFormFactor::SoDimm, - "RDIMM" => MemoryFormFactor::Rdimm, - "LRDIMM" => MemoryFormFactor::Lrdimm, - "UDIMM" => MemoryFormFactor::Udimm, - "NVDIMM" => MemoryFormFactor::Nvdimm, - _ => MemoryFormFactor::Unknown, - } -} -``` - ---- - -## Testing Requirements - -### Unit Tests - -| Test | Description | -|------|-------------| -| `test_parse_dmidecode_memory` | Parse full dmidecode output | -| `test_parse_memory_size` | Size string parsing | -| `test_parse_memory_speed` | Speed string parsing | -| `test_parse_proc_meminfo` | /proc/meminfo parsing | -| `test_memory_type_from_string` | Type enum conversion | -| `test_form_factor_parsing` | Form factor parsing | - -### Integration Tests - -| Test | Platform | Description | -|------|----------|-------------| -| `test_memory_detection` | Linux | Full memory detection | -| `test_memory_without_sudo` | Linux | Fallback without privileges | - ---- - -## References - -### Official Documentation - -| Resource | URL | -|----------|-----| -| JEDEC Standards | https://www.jedec.org/ | -| SMBIOS Specification | https://www.dmtf.org/standards/smbios | -| dmidecode | https://www.nongnu.org/dmidecode/ | -| /proc/meminfo | https://man7.org/linux/man-pages/man5/proc.5.html | - ---- - -## Changelog - -| Date | Changes | -|------|---------| -| 2024-12-29 | Initial specification | diff --git a/docs/NETWORK_ENHANCEMENTS.md b/docs/NETWORK_ENHANCEMENTS.md deleted file mode 100644 index 12efe97..0000000 --- a/docs/NETWORK_ENHANCEMENTS.md +++ /dev/null @@ -1,620 +0,0 @@ -# Network Interface Enhancement Plan - -> **Category:** Data Gap -> **Target Platforms:** Linux (x86_64, aarch64) -> **Priority:** Medium - Missing driver version and operational state - -## Table of Contents - -1. [Problem Statement](#problem-statement) -2. [Current Implementation](#current-implementation) -3. [Entity Changes](#entity-changes) -4. [Detection Method Details](#detection-method-details) -5. [Adapter Implementation](#adapter-implementation) -6. [Parser Implementation](#parser-implementation) -7. [Testing Requirements](#testing-requirements) -8. [References](#references) - ---- - -## Problem Statement - -### Current Issue - -The `NetworkInterface` structure lacks driver information and operational state: - -```rust -// Current struct - missing fields -pub struct NetworkInterface { - pub name: String, - pub mac: String, - pub ip: String, - pub prefix: String, - pub speed: Option, // String, not numeric - pub type_: String, - pub vendor: String, - pub model: String, - pub pci_id: String, - pub numa_node: Option, - // Missing: driver, driver_version, mtu, is_up, is_virtual -} -``` - -### Impact - -- Cannot track NIC driver versions for compatibility -- No MTU information for network configuration validation -- Cannot determine interface operational state -- Cannot distinguish physical vs virtual interfaces - -### Requirements - -1. **Driver information** - driver name and version -2. **Numeric speed** - `speed_mbps: Option` -3. **Operational state** - `is_up: bool` -4. **MTU** - `mtu: u32` -5. **Virtual interface detection** - `is_virtual: bool` - ---- - -## Current Implementation - -### Location - -- **Entity:** `src/domain/entities.rs:263-285` -- **Adapter:** `src/adapters/secondary/system/linux.rs:234-252` -- **Parser:** `src/domain/parsers/network.rs` - -### Current Detection - -``` -┌────────────────────────────────────────────┐ -│ LinuxSystemInfoProvider::get_network_info()│ -└────────────────────────────────────────────┘ - │ - ▼ - ┌──────────────────┐ - │ ip addr show │ - └──────────────────┘ - │ - ▼ - Parse interface list -``` - ---- - -## Entity Changes - -### New NetworkInterface Structure - -```rust -// src/domain/entities.rs - -/// Network interface type classification -/// -/// # References -/// -/// - [Linux Networking](https://www.kernel.org/doc/html/latest/networking/index.html) -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -pub enum NetworkInterfaceType { - /// Physical Ethernet interface - Ethernet, - /// Wireless interface - Wireless, - /// Loopback interface - Loopback, - /// Bridge interface - Bridge, - /// VLAN interface - Vlan, - /// Bond/LAG interface - Bond, - /// Virtual Ethernet (veth pair) - Veth, - /// TUN/TAP interface - TunTap, - /// InfiniBand interface - Infiniband, - /// Macvlan interface - Macvlan, - /// Unknown type - Unknown, -} - -/// Network interface information -/// -/// Represents a network interface with comprehensive metadata for -/// CMDB inventory and network configuration. -/// -/// # Detection Methods -/// -/// Network information is gathered from multiple sources: -/// 1. **sysfs /sys/class/net** - Primary source for most fields -/// 2. **ip command** - Address and routing information -/// 3. **ethtool** - Speed, driver, firmware (requires privileges) -/// -/// # Driver Information -/// -/// The `driver` and `driver_version` fields are essential for: -/// - Compatibility tracking -/// - Firmware update planning -/// - Troubleshooting network issues -/// -/// # Example -/// -/// ``` -/// use hardware_report::NetworkInterface; -/// -/// // Check if interface is usable -/// if iface.is_up && !iface.is_virtual && iface.speed_mbps.unwrap_or(0) >= 10000 { -/// println!("{} is a 10G+ physical interface", iface.name); -/// } -/// ``` -/// -/// # References -/// -/// - [Linux sysfs net](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net) -/// - [ethtool](https://man7.org/linux/man-pages/man8/ethtool.8.html) -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct NetworkInterface { - /// Interface name (e.g., "eth0", "ens192", "enp0s3") - pub name: String, - - /// MAC address in colon-separated format - /// - /// Example: "00:11:22:33:44:55" - pub mac: String, - - /// Permanent MAC address (if different from current) - /// - /// Some NICs allow changing the MAC address. - pub permanent_mac: Option, - - /// Primary IPv4 address - pub ip: String, - - /// IPv4 addresses with prefix length - pub ipv4_addresses: Vec, - - /// IPv6 addresses with prefix length - pub ipv6_addresses: Vec, - - /// Network prefix length (e.g., "24" for /24) - pub prefix: String, - - /// Link speed in Mbps - /// - /// Common values: 1000 (1G), 10000 (10G), 25000 (25G), 100000 (100G) - pub speed_mbps: Option, - - /// Link speed as string (e.g., "10000 Mbps") - pub speed: Option, - - /// Interface type classification - pub interface_type: NetworkInterfaceType, - - /// Interface type as string (for backward compatibility) - pub type_: String, - - /// Hardware vendor name - pub vendor: String, - - /// Hardware model/description - pub model: String, - - /// PCI vendor:device ID (e.g., "8086:1521") - pub pci_id: String, - - /// PCI bus address (e.g., "0000:01:00.0") - pub pci_bus_id: Option, - - /// NUMA node affinity - pub numa_node: Option, - - /// Kernel driver in use - /// - /// Examples: "igb", "i40e", "mlx5_core", "bnxt_en" - pub driver: Option, - - /// Driver version - /// - /// From `/sys/module/{driver}/version` or ethtool. - pub driver_version: Option, - - /// Firmware version - /// - /// From ethtool -i. - pub firmware_version: Option, - - /// Maximum Transmission Unit in bytes - /// - /// Standard: 1500, Jumbo frames: 9000 - pub mtu: u32, - - /// Whether the interface is operationally up - /// - /// From `/sys/class/net/{iface}/operstate`. - pub is_up: bool, - - /// Whether this is a virtual interface - /// - /// Virtual interfaces include: bridges, VLANs, bonds, veths, tun/tap. - pub is_virtual: bool, - - /// Whether this interface is a loopback - pub is_loopback: bool, - - /// Link detected (carrier present) - pub carrier: Option, - - /// Duplex mode: "full", "half", or None if not applicable - pub duplex: Option, - - /// Auto-negotiation status - pub autoneg: Option, - - /// Wake-on-LAN support - pub wake_on_lan: Option, - - /// Transmit queue length - pub tx_queue_len: Option, - - /// Number of RX queues - pub rx_queues: Option, - - /// Number of TX queues - pub tx_queues: Option, - - /// SR-IOV Virtual Functions enabled - pub sriov_numvfs: Option, - - /// Maximum SR-IOV Virtual Functions - pub sriov_totalvfs: Option, -} - -impl Default for NetworkInterface { - fn default() -> Self { - Self { - name: String::new(), - mac: String::new(), - permanent_mac: None, - ip: String::new(), - ipv4_addresses: Vec::new(), - ipv6_addresses: Vec::new(), - prefix: String::new(), - speed_mbps: None, - speed: None, - interface_type: NetworkInterfaceType::Unknown, - type_: String::new(), - vendor: String::new(), - model: String::new(), - pci_id: String::new(), - pci_bus_id: None, - numa_node: None, - driver: None, - driver_version: None, - firmware_version: None, - mtu: 1500, - is_up: false, - is_virtual: false, - is_loopback: false, - carrier: None, - duplex: None, - autoneg: None, - wake_on_lan: None, - tx_queue_len: None, - rx_queues: None, - tx_queues: None, - sriov_numvfs: None, - sriov_totalvfs: None, - } - } -} -``` - ---- - -## Detection Method Details - -### Method 1: sysfs /sys/class/net (Primary) - -**sysfs paths:** - -``` -/sys/class/net/{iface}/ -├── address # MAC address -├── addr_len # Address length -├── mtu # MTU -├── operstate # up/down/unknown -├── carrier # 1=link, 0=no link -├── speed # Speed in Mbps (may be -1) -├── duplex # full/half -├── tx_queue_len # TX queue length -├── type # Interface type (ARPHRD_*) -├── device/ # -> PCI device (if physical) -│ ├── vendor # PCI vendor ID -│ ├── device # PCI device ID -│ ├── numa_node # NUMA affinity -│ ├── driver/ # -> driver symlink -│ │ └── module/ -│ │ └── version # Driver version -│ └── net_dev/queues/ -│ ├── rx-*/ # RX queues -│ └── tx-*/ # TX queues -├── queues/ -│ ├── rx-*/ # RX queues -│ └── tx-*/ # TX queues -└── statistics/ # Interface statistics - ├── rx_bytes - ├── tx_bytes - ├── rx_packets - └── tx_packets -``` - -**Virtual interface detection:** -```rust -fn is_virtual_interface(name: &str, sysfs_path: &Path) -> bool { - // Virtual interfaces don't have a /device symlink - !sysfs_path.join("device").exists() - || name.starts_with("veth") - || name.starts_with("br") - || name.starts_with("virbr") - || name.starts_with("docker") - || name.starts_with("vlan") - || name.contains("bond") - || name.starts_with("tun") - || name.starts_with("tap") -} -``` - -**References:** -- [sysfs-class-net](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net) - ---- - -### Method 2: ethtool - -**When:** For driver/firmware info, detailed link settings - -**Commands:** -```bash -# Driver info -ethtool -i eth0 - -# Link settings -ethtool eth0 - -# Firmware/EEPROM info -ethtool -e eth0 -``` - -**ethtool -i output:** -``` -driver: igb -version: 5.4.0-k -firmware-version: 1.67, 0x80000d38 -bus-info: 0000:01:00.0 -``` - -**References:** -- [ethtool man page](https://man7.org/linux/man-pages/man8/ethtool.8.html) - ---- - -### Method 3: ip command - -**When:** For IP addresses - -**Command:** -```bash -ip -j addr show # JSON output -``` - -**References:** -- [ip command](https://man7.org/linux/man-pages/man8/ip.8.html) - ---- - -## Parser Implementation - -### File: `src/domain/parsers/network.rs` - -```rust -//! Network interface parsing functions -//! -//! # References -//! -//! - [sysfs-class-net](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net) -//! - [ethtool](https://man7.org/linux/man-pages/man8/ethtool.8.html) - -use crate::domain::{NetworkInterface, NetworkInterfaceType}; - -/// Parse sysfs operstate to boolean -/// -/// # Arguments -/// -/// * `content` - Content of `/sys/class/net/{iface}/operstate` -/// -/// # Returns -/// -/// `true` if interface is up. -/// -/// # Example -/// -/// ``` -/// use hardware_report::domain::parsers::network::parse_operstate; -/// -/// assert!(parse_operstate("up")); -/// assert!(!parse_operstate("down")); -/// ``` -pub fn parse_operstate(content: &str) -> bool { - content.trim().to_lowercase() == "up" -} - -/// Parse sysfs speed to Mbps -/// -/// # Arguments -/// -/// * `content` - Content of `/sys/class/net/{iface}/speed` -/// -/// # Returns -/// -/// Speed in Mbps, or None if invalid/unknown. -pub fn parse_sysfs_speed(content: &str) -> Option { - let speed: i32 = content.trim().parse().ok()?; - if speed > 0 { - Some(speed as u32) - } else { - None // -1 means unknown - } -} - -/// Parse ethtool -i output for driver info -/// -/// # Arguments -/// -/// * `output` - Output from `ethtool -i {iface}` -/// -/// # Returns -/// -/// Tuple of (driver, version, firmware_version, bus_info). -/// -/// # References -/// -/// - [ethtool](https://man7.org/linux/man-pages/man8/ethtool.8.html) -pub fn parse_ethtool_driver_info(output: &str) -> (Option, Option, Option, Option) { - let mut driver = None; - let mut version = None; - let mut firmware = None; - let mut bus_info = None; - - for line in output.lines() { - let parts: Vec<&str> = line.splitn(2, ':').collect(); - if parts.len() != 2 { - continue; - } - - let key = parts[0].trim(); - let value = parts[1].trim(); - - match key { - "driver" => driver = Some(value.to_string()), - "version" => version = Some(value.to_string()), - "firmware-version" => firmware = Some(value.to_string()), - "bus-info" => bus_info = Some(value.to_string()), - _ => {} - } - } - - (driver, version, firmware, bus_info) -} - -/// Parse ip -j addr output -/// -/// # Arguments -/// -/// * `output` - JSON output from `ip -j addr show` -/// -/// # References -/// -/// - [ip-address](https://man7.org/linux/man-pages/man8/ip-address.8.html) -pub fn parse_ip_json(output: &str) -> Result, String> { - todo!() -} - -/// Determine interface type from name and sysfs -/// -/// # Arguments -/// -/// * `name` - Interface name -/// * `sysfs_type` - Content of `/sys/class/net/{name}/type` -pub fn determine_interface_type(name: &str, sysfs_type: Option<&str>) -> NetworkInterfaceType { - // Check name patterns first - if name == "lo" { - return NetworkInterfaceType::Loopback; - } - if name.starts_with("br") || name.starts_with("virbr") { - return NetworkInterfaceType::Bridge; - } - if name.starts_with("bond") { - return NetworkInterfaceType::Bond; - } - if name.starts_with("veth") { - return NetworkInterfaceType::Veth; - } - if name.contains(".") || name.starts_with("vlan") { - return NetworkInterfaceType::Vlan; - } - if name.starts_with("tun") || name.starts_with("tap") { - return NetworkInterfaceType::TunTap; - } - if name.starts_with("ib") { - return NetworkInterfaceType::Infiniband; - } - if name.starts_with("wl") || name.starts_with("wlan") { - return NetworkInterfaceType::Wireless; - } - - // Check sysfs type (ARPHRD_* values) - if let Some(type_str) = sysfs_type { - if let Ok(type_num) = type_str.trim().parse::() { - match type_num { - 1 => return NetworkInterfaceType::Ethernet, // ARPHRD_ETHER - 772 => return NetworkInterfaceType::Loopback, // ARPHRD_LOOPBACK - 32 => return NetworkInterfaceType::Infiniband, // ARPHRD_INFINIBAND - _ => {} - } - } - } - - // Default to Ethernet for physical interfaces - if name.starts_with("eth") || name.starts_with("en") { - NetworkInterfaceType::Ethernet - } else { - NetworkInterfaceType::Unknown - } -} -``` - ---- - -## Testing Requirements - -### Unit Tests - -| Test | Description | -|------|-------------| -| `test_parse_operstate` | Parse up/down states | -| `test_parse_sysfs_speed` | Parse speed values | -| `test_parse_ethtool_driver` | Parse ethtool -i output | -| `test_interface_type_detection` | Name to type mapping | -| `test_virtual_interface_detection` | Virtual interface check | - -### Integration Tests - -| Test | Platform | Description | -|------|----------|-------------| -| `test_network_detection` | Linux | Full network detection | -| `test_sysfs_network` | Linux | sysfs parsing | -| `test_ethtool_info` | Linux | ethtool integration | - ---- - -## References - -### Official Documentation - -| Resource | URL | -|----------|-----| -| sysfs-class-net | https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net | -| ethtool | https://man7.org/linux/man-pages/man8/ethtool.8.html | -| ip command | https://man7.org/linux/man-pages/man8/ip.8.html | -| Linux ARPHRD | https://github.com/torvalds/linux/blob/master/include/uapi/linux/if_arp.h | - ---- - -## Changelog - -| Date | Changes | -|------|---------| -| 2024-12-29 | Initial specification | diff --git a/docs/RUSTDOC_STANDARDS.md b/docs/RUSTDOC_STANDARDS.md deleted file mode 100644 index b485fa5..0000000 --- a/docs/RUSTDOC_STANDARDS.md +++ /dev/null @@ -1,560 +0,0 @@ -# Rustdoc Standards and Guidelines - -> **Purpose:** Define documentation standards for the `hardware_report` crate -> **Audience:** Contributors and maintainers - -## Table of Contents - -1. [Overview](#overview) -2. [Documentation Requirements](#documentation-requirements) -3. [Rustdoc Format](#rustdoc-format) -4. [External References](#external-references) -5. [Examples](#examples) -6. [Module Documentation](#module-documentation) -7. [Linting and CI](#linting-and-ci) - ---- - -## Overview - -All public APIs in `hardware_report` must be documented with rustdoc comments. This ensures: - -1. **Discoverability** - Engineers can find what they need -2. **Correctness** - Examples are tested via `cargo test --doc` -3. **Traceability** - Links to official specifications and kernel docs -4. **Maintainability** - Clear contracts for each component - -### Guiding Principles - -- **Every public item gets a doc comment** - structs, enums, functions, traits, modules -- **Link to official references** - kernel docs, hardware specs, crate docs -- **Include examples** - runnable code in doc comments -- **Explain "why" not just "what"** - context for design decisions - ---- - -## Documentation Requirements - -### Required for ALL Public Items - -| Item Type | Required Sections | -|-----------|-------------------| -| Module | Purpose, contents overview | -| Struct | Description, fields, example usage | -| Enum | Description, variants, when to use each | -| Function | Purpose, arguments, returns, errors, example | -| Trait | Purpose, implementors, example | -| Constant | Purpose, value explanation | - -### Required External Links - -When documenting hardware-related items, include links to: - -| Topic | Link To | -|-------|---------| -| sysfs paths | Kernel documentation | -| PCI IDs | pci-ids.ucw.cz | -| SMBIOS fields | DMTF SMBIOS spec | -| NVMe | nvmexpress.org | -| GPU (NVIDIA) | NVIDIA developer docs | -| GPU (AMD) | ROCm documentation | -| Memory specs | JEDEC | -| CPU (x86) | Intel/AMD SDM | -| CPU (ARM) | ARM developer documentation | - ---- - -## Rustdoc Format - -### Basic Structure - -```rust -/// Short one-line description. -/// -/// Longer description that explains the purpose, context, and usage -/// of this item. Can span multiple paragraphs. -/// -/// # Arguments -/// -/// * `param1` - Description of first parameter -/// * `param2` - Description of second parameter -/// -/// # Returns -/// -/// Description of return value. -/// -/// # Errors -/// -/// Description of error conditions. -/// -/// # Panics -/// -/// Conditions under which this function panics (if any). -/// -/// # Safety -/// -/// For unsafe functions, explain the invariants. -/// -/// # Example -/// -/// ```rust -/// use hardware_report::SomeItem; -/// -/// let result = some_function(arg1, arg2); -/// assert!(result.is_ok()); -/// ``` -/// -/// # References -/// -/// - [Link Text](https://url) -/// - [Another Reference](https://url) -pub fn some_function(param1: Type1, param2: Type2) -> Result { - // ... -} -``` - -### Struct Documentation - -```rust -/// GPU device information. -/// -/// Represents a discrete or integrated GPU detected in the system. -/// Memory values are provided in megabytes as unsigned integers for -/// reliable parsing by CMDB consumers. -/// -/// # Detection Methods -/// -/// GPUs are detected using multiple methods in priority order: -/// 1. **NVML** - NVIDIA Management Library (most accurate) -/// 2. **nvidia-smi** - NVIDIA CLI tool -/// 3. **rocm-smi** - AMD GPU tool -/// 4. **sysfs** - Linux `/sys/class/drm` -/// 5. **lspci** - PCI enumeration -/// -/// # Memory Format -/// -/// Memory is reported in **megabytes** as `u64`. The previous string -/// format (e.g., "80 GB") is deprecated. -/// -/// # Example -/// -/// ```rust -/// use hardware_report::GpuDevice; -/// -/// fn process_gpu(gpu: &GpuDevice) { -/// // Calculate memory in GB -/// let memory_gb = gpu.memory_total_mb as f64 / 1024.0; -/// println!("{}: {} GB", gpu.name, memory_gb); -/// -/// // Check vendor -/// if gpu.vendor == GpuVendor::Nvidia { -/// println!("CUDA Compute: {:?}", gpu.compute_capability); -/// } -/// } -/// ``` -/// -/// # References -/// -/// - [NVIDIA NVML API](https://docs.nvidia.com/deploy/nvml-api/) -/// - [AMD ROCm SMI](https://rocm.docs.amd.com/projects/rocm_smi_lib/en/latest/) -/// - [Linux DRM Subsystem](https://www.kernel.org/doc/html/latest/gpu/drm-uapi.html) -/// - [PCI ID Database](https://pci-ids.ucw.cz/) -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct GpuDevice { - /// GPU index (0-based, unique per system). - pub index: u32, - - /// GPU product name. - /// - /// Examples: - /// - "NVIDIA H100 80GB HBM3" - /// - "AMD Instinct MI250X" - pub name: String, - - // ... more fields with individual documentation -} -``` - -### Enum Documentation - -```rust -/// Storage device type classification. -/// -/// Classifies storage devices by their underlying technology. -/// Used for inventory categorization and performance expectations. -/// -/// # Detection -/// -/// Type is determined by: -/// 1. Device name prefix (`nvme*`, `sd*`, `mmcblk*`) -/// 2. sysfs rotational flag -/// 3. Interface type -/// -/// # Example -/// -/// ```rust -/// use hardware_report::StorageType; -/// -/// let device_type = StorageType::from_device("nvme0n1", false); -/// assert_eq!(device_type, StorageType::Nvme); -/// -/// match device_type { -/// StorageType::Nvme | StorageType::Ssd => println!("Fast storage"), -/// StorageType::Hdd => println!("Rotational storage"), -/// _ => println!("Other"), -/// } -/// ``` -/// -/// # References -/// -/// - [Linux Block Devices](https://www.kernel.org/doc/html/latest/block/index.html) -/// - [NVMe Specification](https://nvmexpress.org/specifications/) -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -pub enum StorageType { - /// NVMe solid-state drive. - /// - /// Detected by `nvme*` device name prefix. - /// Typically provides highest performance (PCIe interface). - Nvme, - - /// SATA/SAS solid-state drive. - /// - /// Detected by `rotational=0` on `sd*` devices. - Ssd, - - /// Hard disk drive (rotational media). - /// - /// Detected by `rotational=1` on `sd*` devices. - Hdd, - - /// Embedded MMC storage. - /// - /// Common on ARM platforms. Detected by `mmcblk*` prefix. - Emmc, - - /// Unknown or unclassified storage type. - Unknown, -} -``` - -### Function Documentation - -```rust -/// Parse sysfs frequency file to MHz. -/// -/// Converts kernel cpufreq values (in kHz) to MHz for consistent -/// representation across the crate. -/// -/// # Arguments -/// -/// * `content` - Content of a cpufreq file (e.g., `scaling_max_freq`) -/// -/// # Returns -/// -/// Frequency in MHz as `u32`. -/// -/// # Errors -/// -/// Returns an error if the content cannot be parsed as an integer. -/// -/// # Example -/// -/// ```rust -/// use hardware_report::domain::parsers::cpu::parse_sysfs_freq_khz; -/// -/// // 3.5 GHz in kHz -/// let freq_mhz = parse_sysfs_freq_khz("3500000").unwrap(); -/// assert_eq!(freq_mhz, 3500); -/// -/// // Invalid input -/// assert!(parse_sysfs_freq_khz("invalid").is_err()); -/// ``` -/// -/// # References -/// -/// - [cpufreq sysfs](https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.rst) -pub fn parse_sysfs_freq_khz(content: &str) -> Result { - let khz: u32 = content - .trim() - .parse() - .map_err(|e| format!("Invalid frequency: {}", e))?; - Ok(khz / 1000) -} -``` - ---- - -## External References - -### Reference Link Format - -Use markdown links in the `# References` section: - -```rust -/// # References -/// -/// - [Link Text](https://full.url.here) -``` - -### Standard Reference URLs - -#### Linux Kernel - -| Topic | URL Pattern | -|-------|-------------| -| sysfs ABI | `https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-*` | -| Block devices | `https://www.kernel.org/doc/html/latest/block/index.html` | -| Networking | `https://www.kernel.org/doc/html/latest/networking/index.html` | -| DRM/GPU | `https://www.kernel.org/doc/html/latest/gpu/drm-uapi.html` | -| CPU | `https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.rst` | - -#### Hardware Specifications - -| Topic | URL | -|-------|-----| -| NVMe | `https://nvmexpress.org/specifications/` | -| SMBIOS | `https://www.dmtf.org/standards/smbios` | -| PCI IDs | `https://pci-ids.ucw.cz/` | -| JEDEC (Memory) | `https://www.jedec.org/` | - -#### Vendor Documentation - -| Vendor | Topic | URL | -|--------|-------|-----| -| NVIDIA | NVML | `https://docs.nvidia.com/deploy/nvml-api/` | -| NVIDIA | CUDA CC | `https://developer.nvidia.com/cuda-gpus` | -| AMD | ROCm SMI | `https://rocm.docs.amd.com/projects/rocm_smi_lib/en/latest/` | -| Intel | CPUID | `https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html` | -| ARM | CPU ID | `https://developer.arm.com/documentation/ddi0487/latest` | - -#### Crate Documentation - -| Crate | URL | -|-------|-----| -| nvml-wrapper | `https://docs.rs/nvml-wrapper` | -| raw-cpuid | `https://docs.rs/raw-cpuid` | -| sysinfo | `https://docs.rs/sysinfo` | -| serde | `https://docs.rs/serde` | - -### Intra-doc Links - -Use Rust's intra-doc links to reference other items in the crate: - -```rust -/// See [`GpuDevice`] for GPU information. -/// See [`StorageType::Nvme`] for NVMe detection. -/// See [`parse_sysfs_size`](crate::domain::parsers::storage::parse_sysfs_size). -``` - ---- - -## Examples - -### Testable Examples - -All examples in doc comments should be testable: - -```rust -/// # Example -/// -/// ```rust -/// use hardware_report::StorageType; -/// -/// let st = StorageType::from_device("nvme0n1", false); -/// assert_eq!(st, StorageType::Nvme); -/// ``` -``` - -Run with: -```bash -cargo test --doc -``` - -### Non-runnable Examples - -For examples that can't be run (require hardware, external commands): - -```rust -/// # Example -/// -/// ```rust,no_run -/// use hardware_report::create_service; -/// -/// #[tokio::main] -/// async fn main() { -/// let service = create_service().unwrap(); -/// let report = service.generate_report(Default::default()).await.unwrap(); -/// println!("{:?}", report); -/// } -/// ``` -``` - -### Examples That Should Not Compile - -For showing incorrect usage: - -```rust -/// # Example of what NOT to do -/// -/// ```rust,compile_fail -/// // This won't compile because memory is u64, not String -/// let memory: String = gpu.memory_total_mb; -/// ``` -``` - ---- - -## Module Documentation - -### Module-Level Documentation - -Every module should have a `//!` comment at the top: - -```rust -//! GPU information parsing functions. -//! -//! This module provides pure parsing functions for GPU information from -//! various sources. All functions take string input and return parsed -//! results without performing I/O. -//! -//! # Supported Formats -//! -//! - nvidia-smi CSV output -//! - rocm-smi JSON output -//! - lspci text output -//! - sysfs file contents -//! -//! # Architecture -//! -//! These functions are part of the **domain layer** in the hexagonal -//! architecture. They have no dependencies on adapters or I/O. -//! -//! # Example -//! -//! ```rust -//! use hardware_report::domain::parsers::gpu::parse_nvidia_smi_output; -//! -//! let output = "0, NVIDIA H100, GPU-xxx, 81920, 81000"; -//! let gpus = parse_nvidia_smi_output(output).unwrap(); -//! assert_eq!(gpus[0].memory_total_mb, 81920); -//! ``` -//! -//! # References -//! -//! - [nvidia-smi](https://developer.nvidia.com/nvidia-system-management-interface) -//! - [rocm-smi](https://rocm.docs.amd.com/projects/rocm_smi_lib/en/latest/) - -use crate::domain::{GpuDevice, GpuVendor}; - -// ... module contents -``` - -### Re-exports Documentation - -Document re-exports in `lib.rs`: - -```rust -//! # Hardware Report -//! -//! A library for collecting hardware information on Linux systems. -//! -//! ## Quick Start -//! -//! ```rust,no_run -//! use hardware_report::{create_service, ReportConfig}; -//! -//! #[tokio::main] -//! async fn main() -> Result<(), Box> { -//! let service = create_service()?; -//! let report = service.generate_report(ReportConfig::default()).await?; -//! println!("Hostname: {}", report.hostname); -//! Ok(()) -//! } -//! ``` -//! -//! ## Architecture -//! -//! This crate follows the Hexagonal Architecture (Ports and Adapters): -//! -//! - **Domain**: Core entities and pure parsing functions -//! - **Ports**: Trait definitions for required/provided interfaces -//! - **Adapters**: Platform-specific implementations -//! -//! ## Feature Flags -//! -//! - `nvidia` - Enable NVML support for NVIDIA GPUs -//! - `x86-cpu` - Enable raw-cpuid for x86 CPU detection - -pub use domain::entities::*; -pub use domain::errors::*; -``` - ---- - -## Linting and CI - -### Rustdoc Lints - -Enable documentation lints in `lib.rs`: - -```rust -#![warn(missing_docs)] -#![warn(rustdoc::broken_intra_doc_links)] -#![warn(rustdoc::private_intra_doc_links)] -#![warn(rustdoc::missing_crate_level_docs)] -#![warn(rustdoc::invalid_codeblock_attributes)] -#![warn(rustdoc::invalid_html_tags)] -``` - -### CI Checks - -Add to CI workflow: - -```yaml -- name: Check documentation - run: | - cargo doc --no-deps --document-private-items - cargo test --doc - -- name: Check for broken links - run: | - cargo rustdoc -- -D warnings -``` - -### Local Documentation - -Generate and view docs locally: - -```bash -# Generate docs -cargo doc --no-deps --open - -# Generate with private items -cargo doc --no-deps --document-private-items --open - -# Test doc examples -cargo test --doc -``` - ---- - -## Checklist - -Before submitting code, verify: - -- [ ] All public items have `///` doc comments -- [ ] Modules have `//!` documentation -- [ ] Examples compile and pass (`cargo test --doc`) -- [ ] External references are included where relevant -- [ ] Intra-doc links work (`cargo doc` succeeds) -- [ ] `#[deprecated]` items explain migration path -- [ ] Complex types have usage examples -- [ ] Error conditions are documented - ---- - -## Changelog - -| Date | Changes | -|------|---------| -| 2024-12-29 | Initial standards document | diff --git a/docs/STORAGE_DETECTION.md b/docs/STORAGE_DETECTION.md deleted file mode 100644 index 484d8fa..0000000 --- a/docs/STORAGE_DETECTION.md +++ /dev/null @@ -1,1136 +0,0 @@ -# Storage Detection Enhancement Plan - -> **Category:** Critical Issue -> **Target Platforms:** Linux (x86_64, aarch64) -> **Priority:** Critical - Storage returns empty on ARM platforms - -## Table of Contents - -1. [Problem Statement](#problem-statement) -2. [Current Implementation](#current-implementation) -3. [Multi-Method Detection Strategy](#multi-method-detection-strategy) -4. [Entity Changes](#entity-changes) -5. [Detection Method Details](#detection-method-details) -6. [Adapter Implementation](#adapter-implementation) -7. [Parser Implementation](#parser-implementation) -8. [ARM/aarch64 Considerations](#armaarch64-considerations) -9. [Testing Requirements](#testing-requirements) -10. [References](#references) - ---- - -## Problem Statement - -### Current Issue - -The `hardware_report` crate returns an empty storage array on ARM/aarch64 platforms: - -```rust -// Current output on ARM -StorageInfo { - devices: [], // Empty! -} -``` - -Additionally, the current `StorageDevice` structure lacks critical fields for CMDB: - -```rust -// Current struct - missing fields -pub struct StorageDevice { - pub name: String, - pub type_: String, // String, not enum - pub size: String, // String, not numeric - pub model: String, - // Missing: serial_number, firmware_version, interface, etc. -} -``` - -### Impact - -- No storage inventory on ARM platforms (DGX Spark, Graviton, etc.) -- CMDB cannot track storage serial numbers for asset management -- No firmware version for compliance tracking -- Size as string breaks automated capacity calculations - -### Requirements - -1. **Reliable detection on ARM/aarch64** - Primary target platform -2. **Numeric size fields** - `size_bytes: u64` and `size_gb: f64` -3. **Serial number extraction** - For asset tracking (may require privileges) -4. **Firmware version** - For compliance and update tracking -5. **Multi-method fallback** - sysfs primary, lsblk secondary, sysinfo tertiary - ---- - -## Current Implementation - -### Location - -- **Entity:** `src/domain/entities.rs:207-225` -- **Adapter:** `src/adapters/secondary/system/linux.rs:153-170` -- **Parser:** `src/domain/parsers/storage.rs` - -### Current Detection Flow - -``` -┌─────────────────────────────────────────────┐ -│ LinuxSystemInfoProvider::get_storage_info() │ -└─────────────────────────────────────────────┘ - │ - ▼ - ┌──────────────────────┐ - │ lsblk -d -o │ - │ NAME,SIZE,TYPE │ - └──────────────────────┘ - │ - ▼ - ┌──────────────────────┐ - │ Parse text output │ - │ (whitespace split) │ - └──────────────────────┘ - │ - ▼ - Return devices - (may be empty!) -``` - -### Why It Fails on ARM - -1. **lsblk output format differs** - Column ordering/presence varies -2. **No fallback** - If lsblk fails, no alternative tried -3. **Parsing assumes columns** - `parts[3]` for size fails if fewer columns -4. **No sysfs fallback** - Most reliable source not used - ---- - -## Multi-Method Detection Strategy - -### Detection Priority Chain - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ STORAGE DETECTION CHAIN │ -│ │ -│ Priority 1: sysfs /sys/block (Linux) │ -│ ├── Most reliable across architectures │ -│ ├── Direct kernel interface │ -│ ├── Works on x86_64 and aarch64 │ -│ ├── Serial/firmware may require elevated privileges │ -│ └── Paths: │ -│ ├── /sys/block/{dev}/size │ -│ ├── /sys/block/{dev}/device/model │ -│ ├── /sys/block/{dev}/device/serial │ -│ ├── /sys/block/{dev}/device/firmware_rev │ -│ └── /sys/block/{dev}/queue/rotational │ -│ │ │ -│ ▼ (enrich with additional data) │ -│ Priority 2: lsblk JSON output │ -│ ├── Structured output format │ -│ ├── Additional fields (FSTYPE, MOUNTPOINT) │ -│ └── Command: lsblk -J -o NAME,SIZE,TYPE,MODEL,SERIAL,ROTA │ -│ │ │ -│ ▼ (if lsblk unavailable) │ -│ Priority 3: NVMe CLI (for NVMe devices) │ -│ ├── Detailed NVMe information │ -│ ├── Firmware version │ -│ └── Command: nvme list -o json │ -│ │ │ -│ ▼ (cross-platform fallback) │ -│ Priority 4: sysinfo crate │ -│ ├── Cross-platform disk enumeration │ -│ ├── Limited metadata │ -│ └── Good for basic size/mount info │ -│ │ │ -│ ▼ (for serial numbers if other methods fail) │ -│ Priority 5: smartctl (SMART data) │ -│ ├── Serial number │ -│ ├── Firmware version │ -│ ├── Health status │ -│ └── Requires smartmontools package │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -### Method Capabilities Matrix - -| Method | Size | Model | Serial | Firmware | Type | Rotational | NVMe-specific | -|--------|------|-------|--------|----------|------|------------|---------------| -| sysfs | Yes | Yes | Maybe* | Maybe* | Yes | Yes | Partial | -| lsblk | Yes | Yes | Maybe* | No | Yes | Yes | No | -| nvme-cli | Yes | Yes | Yes | Yes | NVMe only | No | Yes | -| sysinfo | Yes | No | No | No | Limited | No | No | -| smartctl | Yes | Yes | Yes | Yes | Yes | Yes | Yes | - -*Requires elevated privileges or specific kernel configuration - ---- - -## Entity Changes - -### New StorageType Enum - -```rust -// src/domain/entities.rs - -/// Storage device type classification -/// -/// Classifies storage devices by their underlying technology and interface. -/// -/// # Detection -/// -/// Type is determined by: -/// 1. Device name prefix (nvme*, sd*, mmcblk*) -/// 2. sysfs rotational flag -/// 3. Interface type from sysfs -/// -/// # References -/// -/// - [Linux Block Devices](https://www.kernel.org/doc/html/latest/block/index.html) -/// - [NVMe Specification](https://nvmexpress.org/specifications/) -/// - [SATA Specification](https://sata-io.org/developers/sata-revision-3-5-specification) -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -pub enum StorageType { - /// NVMe solid-state drive - /// - /// Detected by device name starting with "nvme" or interface type. - /// Typically provides highest performance. - Nvme, - - /// SATA/SAS solid-state drive - /// - /// Detected by rotational=0 on sd* devices. - Ssd, - - /// Hard disk drive (rotational media) - /// - /// Detected by rotational=1 on sd* devices. - Hdd, - - /// Embedded MMC storage - /// - /// Common on ARM platforms (eMMC). Detected by mmcblk* device name. - Emmc, - - /// Virtual or memory-backed device - /// - /// Includes RAM disks, loop devices, and device-mapper devices. - Virtual, - - /// Unknown or unclassified storage type - Unknown, -} - -impl StorageType { - /// Determine storage type from device name and rotational flag - /// - /// # Arguments - /// - /// * `device_name` - Block device name (e.g., "nvme0n1", "sda", "mmcblk0") - /// * `is_rotational` - Whether the device uses rotational media - /// - /// # Example - /// - /// ``` - /// use hardware_report::StorageType; - /// - /// assert_eq!(StorageType::from_device("nvme0n1", false), StorageType::Nvme); - /// assert_eq!(StorageType::from_device("sda", false), StorageType::Ssd); - /// assert_eq!(StorageType::from_device("sda", true), StorageType::Hdd); - /// assert_eq!(StorageType::from_device("mmcblk0", false), StorageType::Emmc); - /// ``` - pub fn from_device(device_name: &str, is_rotational: bool) -> Self { - if device_name.starts_with("nvme") { - StorageType::Nvme - } else if device_name.starts_with("mmcblk") { - StorageType::Emmc - } else if device_name.starts_with("loop") - || device_name.starts_with("ram") - || device_name.starts_with("dm-") - { - StorageType::Virtual - } else if is_rotational { - StorageType::Hdd - } else if device_name.starts_with("sd") || device_name.starts_with("vd") { - StorageType::Ssd - } else { - StorageType::Unknown - } - } - - /// Get human-readable display name - pub fn display_name(&self) -> &'static str { - match self { - StorageType::Nvme => "NVMe SSD", - StorageType::Ssd => "SSD", - StorageType::Hdd => "HDD", - StorageType::Emmc => "eMMC", - StorageType::Virtual => "Virtual", - StorageType::Unknown => "Unknown", - } - } -} - -impl std::fmt::Display for StorageType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.display_name()) - } -} -``` - -### New StorageDevice Structure - -```rust -// src/domain/entities.rs - -/// Storage device information -/// -/// Represents a block storage device detected in the system. Provides both -/// numeric and string representations of size for flexibility. -/// -/// # Detection Methods -/// -/// Storage devices are detected using multiple methods in priority order: -/// 1. **sysfs** - `/sys/block` interface (most reliable on Linux) -/// 2. **lsblk** - Block device listing command -/// 3. **nvme-cli** - NVMe-specific tooling -/// 4. **sysinfo** - Cross-platform crate fallback -/// 5. **smartctl** - SMART data for enrichment -/// -/// # Filtering -/// -/// Virtual devices (loop, ram, dm-*) are excluded by default. Use the -/// `include_virtual` configuration option to include them. -/// -/// # Privileges -/// -/// Some fields (serial_number, firmware_version) may require elevated -/// privileges (root/sudo) to read. These will be `None` if inaccessible. -/// -/// # Example -/// -/// ``` -/// use hardware_report::StorageDevice; -/// -/// // Size is available in multiple formats -/// let size_tb = device.size_bytes as f64 / (1024.0 * 1024.0 * 1024.0 * 1024.0); -/// let size_gb = device.size_gb; // Pre-calculated convenience field -/// ``` -/// -/// # References -/// -/// - [Linux sysfs block ABI](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-block) -/// - [NVMe CLI](https://github.com/linux-nvme/nvme-cli) -/// - [smartmontools](https://www.smartmontools.org/) -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct StorageDevice { - /// Block device name (e.g., "nvme0n1", "sda") - /// - /// This is the kernel device name without the `/dev/` prefix. - pub name: String, - - /// Full device path (e.g., "/dev/nvme0n1") - pub device_path: String, - - /// Storage device type classification - pub device_type: StorageType, - - /// Device size in bytes - /// - /// Calculated from sysfs `size` (512-byte sectors) or other sources. - /// This is the raw capacity, not the formatted/usable capacity. - pub size_bytes: u64, - - /// Device size in gigabytes (convenience field) - /// - /// Calculated as `size_bytes / (1024^3)`. Note this uses binary GB (GiB). - pub size_gb: f64, - - /// Device size in terabytes (convenience field) - /// - /// Calculated as `size_bytes / (1024^4)`. Note this uses binary TB (TiB). - pub size_tb: f64, - - /// Device model name - /// - /// From sysfs `/sys/block/{dev}/device/model` or equivalent. - /// May include trailing whitespace from hardware. - pub model: String, - - /// Device serial number - /// - /// Important for asset tracking and CMDB inventory. - /// - /// # Note - /// - /// May require elevated privileges to read. Returns `None` if - /// inaccessible or not available. - /// - /// # Sources - /// - /// - sysfs: `/sys/block/{dev}/device/serial` - /// - NVMe: `/sys/class/nvme/{ctrl}/serial` - /// - smartctl: `smartctl -i /dev/{dev}` - pub serial_number: Option, - - /// Device firmware version - /// - /// Important for compliance tracking and identifying devices - /// that need firmware updates. - /// - /// # Sources - /// - /// - sysfs: `/sys/block/{dev}/device/firmware_rev` - /// - NVMe: `/sys/class/nvme/{ctrl}/firmware_rev` - /// - smartctl: `smartctl -i /dev/{dev}` - pub firmware_version: Option, - - /// Interface type - /// - /// Examples: "NVMe", "SATA", "SAS", "USB", "eMMC", "virtio" - pub interface: String, - - /// Whether the device uses rotational media - /// - /// - `true` = HDD (spinning platters) - /// - `false` = SSD/NVMe/eMMC (solid state) - /// - /// Read from sysfs `/sys/block/{dev}/queue/rotational`. - pub is_rotational: bool, - - /// World Wide Name (WWN) if available - /// - /// A globally unique identifier for the device. Format varies: - /// - SATA: NAA format (e.g., "0x5000c5004567890a") - /// - NVMe: EUI-64 or NGUID - /// - /// # Sources - /// - /// - sysfs: `/sys/block/{dev}/device/wwid` - /// - lsblk: WWN column - pub wwn: Option, - - /// NVMe Namespace ID (NVMe devices only) - /// - /// For NVMe devices, this identifies the namespace within the controller. - /// Typically 1 for single-namespace devices. - pub nvme_namespace: Option, - - /// SMART health status - /// - /// Indicates overall device health based on SMART data. - /// Values: "PASSED", "FAILED", or `None` if unavailable. - pub smart_status: Option, - - /// Transport protocol - /// - /// More specific than `interface`. Examples: - /// - "PCIe 4.0 x4" (NVMe) - /// - "SATA 6Gb/s" - /// - "SAS 12Gb/s" - pub transport: Option, - - /// Logical block size in bytes - /// - /// Typically 512 or 4096. Affects alignment requirements. - pub block_size: Option, - - /// Physical block size in bytes - /// - /// May differ from logical block size (e.g., 4Kn drives). - pub physical_block_size: Option, - - /// Detection method that discovered this device - /// - /// One of: "sysfs", "lsblk", "nvme-cli", "sysinfo", "smartctl" - pub detection_method: String, -} - -impl Default for StorageDevice { - fn default() -> Self { - Self { - name: String::new(), - device_path: String::new(), - device_type: StorageType::Unknown, - size_bytes: 0, - size_gb: 0.0, - size_tb: 0.0, - model: String::new(), - serial_number: None, - firmware_version: None, - interface: "Unknown".to_string(), - is_rotational: false, - wwn: None, - nvme_namespace: None, - smart_status: None, - transport: None, - block_size: None, - physical_block_size: None, - detection_method: String::new(), - } - } -} - -impl StorageDevice { - /// Calculate size fields from bytes - /// - /// Updates `size_gb` and `size_tb` based on `size_bytes`. - pub fn calculate_size_fields(&mut self) { - const GB: f64 = 1024.0 * 1024.0 * 1024.0; - const TB: f64 = GB * 1024.0; - self.size_gb = self.size_bytes as f64 / GB; - self.size_tb = self.size_bytes as f64 / TB; - } -} -``` - ---- - -## Detection Method Details - -### Method 1: sysfs /sys/block (Primary) - -**When:** Linux systems (always attempted first) - -**sysfs paths for each device:** - -``` -/sys/block/{device}/ -├── size # Size in 512-byte sectors -├── queue/ -│ ├── rotational # 0=SSD, 1=HDD -│ ├── logical_block_size # Logical block size -│ └── physical_block_size # Physical block size -├── device/ -│ ├── model # Device model (may have trailing spaces) -│ ├── vendor # Device vendor -│ ├── serial # Serial number (may need root) -│ ├── firmware_rev # Firmware version -│ └── wwid # World Wide Name -└── ... (other attributes) -``` - -**NVMe-specific paths:** - -``` -/sys/class/nvme/{controller}/ -├── serial # Controller serial number -├── model # Controller model -├── firmware_rev # Firmware revision -└── transport # Transport type (pcie, tcp, rdma) - -/sys/class/nvme/{controller}/nvme{X}n{Y}/ -├── size # Namespace size -├── wwid # Namespace WWID -└── ... -``` - -**Size calculation:** - -```rust -// sysfs reports size in 512-byte sectors -let sectors: u64 = read_sysfs_file("/sys/block/sda/size")?.parse()?; -let size_bytes = sectors * 512; -``` - -**References:** -- [sysfs-block ABI](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-block) -- [sysfs-class-nvme](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-nvme) - ---- - -### Method 2: lsblk JSON Output - -**When:** Enrichment after sysfs, or if sysfs incomplete - -**Command:** -```bash -lsblk -J -o NAME,SIZE,TYPE,MODEL,SERIAL,ROTA,TRAN,WWN,FSTYPE,MOUNTPOINT -b -``` - -**Output format:** -```json -{ - "blockdevices": [ - { - "name": "nvme0n1", - "size": 2000398934016, - "type": "disk", - "model": "Samsung SSD 980 PRO 2TB", - "serial": "S5GXNF0N123456", - "rota": false, - "tran": "nvme", - "wwn": "eui.0025385b21404321" - } - ] -} -``` - -**Note:** The `-b` flag outputs size in bytes, avoiding parsing human-readable formats. - -**References:** -- [lsblk man page](https://man7.org/linux/man-pages/man8/lsblk.8.html) -- [util-linux source](https://github.com/util-linux/util-linux) - ---- - -### Method 3: NVMe CLI - -**When:** NVMe devices detected, nvme-cli available - -**Command:** -```bash -nvme list -o json -``` - -**Output format:** -```json -{ - "Devices": [ - { - "DevicePath": "/dev/nvme0n1", - "Firmware": "1B2QGXA7", - "ModelNumber": "Samsung SSD 980 PRO 2TB", - "SerialNumber": "S5GXNF0N123456", - "PhysicalSize": 2000398934016, - "UsedBytes": 1500000000000 - } - ] -} -``` - -**References:** -- [nvme-cli GitHub](https://github.com/linux-nvme/nvme-cli) -- [NVMe Specification](https://nvmexpress.org/specifications/) - ---- - -### Method 4: sysinfo Crate - -**When:** Cross-platform fallback, or other methods unavailable - -**Usage:** -```rust -use sysinfo::Disks; - -let disks = Disks::new_with_refreshed_list(); -for disk in disks.iter() { - let name = disk.name().to_string_lossy(); - let size = disk.total_space(); - let fs_type = disk.file_system().to_string_lossy(); - let mount_point = disk.mount_point(); -} -``` - -**Limitations:** -- Reports mounted filesystems, not raw block devices -- No serial number or firmware version -- Limited device type detection - -**References:** -- [sysinfo crate](https://docs.rs/sysinfo) - ---- - -### Method 5: smartctl - -**When:** Serial/firmware needed and not available from sysfs - -**Command:** -```bash -smartctl -i /dev/sda --json -``` - -**Output format:** -```json -{ - "model_name": "Samsung SSD 870 EVO 2TB", - "serial_number": "S5XXNX0N123456", - "firmware_version": "SVT01B6Q", - "smart_status": { - "passed": true - } -} -``` - -**Note:** Requires `smartmontools` package and often root privileges. - -**References:** -- [smartmontools](https://www.smartmontools.org/) -- [smartctl man page](https://linux.die.net/man/8/smartctl) - ---- - -## Adapter Implementation - -### File: `src/adapters/secondary/system/linux.rs` - -```rust -// Pseudocode for new implementation - -impl SystemInfoProvider for LinuxSystemInfoProvider { - async fn get_storage_info(&self) -> Result { - let mut devices = Vec::new(); - - // Method 1: sysfs (primary) - match self.detect_storage_sysfs().await { - Ok(sysfs_devices) => { - log::debug!("Found {} devices via sysfs", sysfs_devices.len()); - devices = sysfs_devices; - } - Err(e) => { - log::warn!("sysfs storage detection failed: {}", e); - } - } - - // Method 2: lsblk enrichment - if let Ok(lsblk_devices) = self.detect_storage_lsblk().await { - self.merge_storage_info(&mut devices, lsblk_devices); - } - - // Method 3: NVMe CLI enrichment (for NVMe devices) - if devices.iter().any(|d| d.device_type == StorageType::Nvme) { - if let Ok(nvme_devices) = self.detect_storage_nvme_cli().await { - self.merge_storage_info(&mut devices, nvme_devices); - } - } - - // Method 4: sysinfo fallback (if no devices found) - if devices.is_empty() { - if let Ok(sysinfo_devices) = self.detect_storage_sysinfo().await { - devices = sysinfo_devices; - } - } - - // Method 5: smartctl enrichment (for missing serial/firmware) - for device in &mut devices { - if device.serial_number.is_none() || device.firmware_version.is_none() { - if let Ok(smart_info) = self.get_smart_info(&device.name).await { - self.merge_smart_info(device, smart_info); - } - } - } - - // Filter out virtual devices (configurable) - devices.retain(|d| d.device_type != StorageType::Virtual); - - // Calculate convenience fields - for device in &mut devices { - device.calculate_size_fields(); - } - - Ok(StorageInfo { devices }) - } -} -``` - -### Helper Methods - -```rust -impl LinuxSystemInfoProvider { - /// Detect storage devices via sysfs - /// - /// Primary detection method for Linux. Reads directly from - /// `/sys/block` kernel interface. - /// - /// # References - /// - /// - [sysfs-block ABI](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-block) - async fn detect_storage_sysfs(&self) -> Result, SystemError> { - // Read /sys/block directory - // For each entry, read attributes - // Build StorageDevice - todo!() - } - - /// Detect storage devices via lsblk command - /// - /// # Requirements - /// - /// - `lsblk` must be in PATH (util-linux package) - async fn detect_storage_lsblk(&self) -> Result, SystemError> { - let cmd = SystemCommand::new("lsblk") - .args(&["-J", "-o", "NAME,SIZE,TYPE,MODEL,SERIAL,ROTA,TRAN,WWN", "-b"]) - .timeout(Duration::from_secs(10)); - - let output = self.command_executor.execute(&cmd).await?; - parse_lsblk_json(&output.stdout).map_err(SystemError::ParseError) - } - - /// Detect NVMe devices via nvme-cli - /// - /// # Requirements - /// - /// - `nvme` must be in PATH (nvme-cli package) - async fn detect_storage_nvme_cli(&self) -> Result, SystemError> { - let cmd = SystemCommand::new("nvme") - .args(&["list", "-o", "json"]) - .timeout(Duration::from_secs(10)); - - let output = self.command_executor.execute(&cmd).await?; - parse_nvme_list_json(&output.stdout).map_err(SystemError::ParseError) - } - - /// Detect storage via sysinfo crate - /// - /// Cross-platform fallback with limited information. - async fn detect_storage_sysinfo(&self) -> Result, SystemError> { - use sysinfo::Disks; - - let disks = Disks::new_with_refreshed_list(); - let mut devices = Vec::new(); - - for disk in disks.iter() { - // Convert sysinfo disk to StorageDevice - // ... - } - - Ok(devices) - } - - /// Get SMART information for a device - /// - /// # Requirements - /// - /// - `smartctl` must be in PATH (smartmontools package) - /// - Often requires root privileges - async fn get_smart_info(&self, device_name: &str) -> Result { - let cmd = SystemCommand::new("smartctl") - .args(&["-i", "--json", &format!("/dev/{}", device_name)]) - .timeout(Duration::from_secs(10)); - - let output = self.command_executor.execute_with_privileges(&cmd).await?; - parse_smartctl_json(&output.stdout).map_err(SystemError::ParseError) - } - - /// Merge storage info from secondary source - /// - /// Matches devices by name and fills in missing fields. - fn merge_storage_info(&self, primary: &mut Vec, secondary: Vec) { - for sec_dev in secondary { - if let Some(pri_dev) = primary.iter_mut().find(|d| d.name == sec_dev.name) { - // Fill in missing fields - if pri_dev.serial_number.is_none() { - pri_dev.serial_number = sec_dev.serial_number; - } - if pri_dev.firmware_version.is_none() { - pri_dev.firmware_version = sec_dev.firmware_version; - } - // ... other fields - } else { - // Device not in primary, add it - primary.push(sec_dev); - } - } - } -} -``` - ---- - -## Parser Implementation - -### File: `src/domain/parsers/storage.rs` - -```rust -//! Storage information parsing functions -//! -//! This module provides pure parsing functions for storage device information -//! from various sources. All functions take string input and return parsed -//! results without performing I/O. -//! -//! # Supported Formats -//! -//! - sysfs file contents -//! - lsblk JSON output -//! - nvme-cli JSON output -//! - smartctl JSON output -//! -//! # References -//! -//! - [Linux sysfs block](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-block) -//! - [lsblk JSON format](https://github.com/util-linux/util-linux) - -use crate::domain::{StorageDevice, StorageType}; - -/// Parse sysfs size file to bytes -/// -/// # Arguments -/// -/// * `content` - Content of `/sys/block/{dev}/size` file -/// -/// # Returns -/// -/// Size in bytes. sysfs reports size in 512-byte sectors. -/// -/// # Example -/// -/// ``` -/// use hardware_report::domain::parsers::storage::parse_sysfs_size; -/// -/// let size_bytes = parse_sysfs_size("3907029168").unwrap(); -/// assert_eq!(size_bytes, 3907029168 * 512); // ~2TB -/// ``` -pub fn parse_sysfs_size(content: &str) -> Result { - let sectors: u64 = content - .trim() - .parse() - .map_err(|e| format!("Failed to parse size: {}", e))?; - Ok(sectors * 512) -} - -/// Parse sysfs rotational flag -/// -/// # Arguments -/// -/// * `content` - Content of `/sys/block/{dev}/queue/rotational` file -/// -/// # Returns -/// -/// `true` if device is rotational (HDD), `false` for SSD/NVMe. -/// -/// # Example -/// -/// ``` -/// use hardware_report::domain::parsers::storage::parse_sysfs_rotational; -/// -/// assert_eq!(parse_sysfs_rotational("1"), true); // HDD -/// assert_eq!(parse_sysfs_rotational("0"), false); // SSD -/// ``` -pub fn parse_sysfs_rotational(content: &str) -> bool { - content.trim() == "1" -} - -/// Parse lsblk JSON output -/// -/// # Arguments -/// -/// * `output` - JSON output from `lsblk -J -o NAME,SIZE,TYPE,MODEL,SERIAL,ROTA,TRAN,WWN -b` -/// -/// # Returns -/// -/// Vector of storage devices parsed from lsblk output. -/// -/// # Expected Format -/// -/// ```json -/// { -/// "blockdevices": [ -/// {"name": "sda", "size": 1000204886016, "type": "disk", ...} -/// ] -/// } -/// ``` -/// -/// # References -/// -/// - [lsblk man page](https://man7.org/linux/man-pages/man8/lsblk.8.html) -pub fn parse_lsblk_json(output: &str) -> Result, String> { - todo!() -} - -/// Parse nvme-cli list JSON output -/// -/// # Arguments -/// -/// * `output` - JSON output from `nvme list -o json` -/// -/// # Returns -/// -/// Vector of NVMe storage devices. -/// -/// # Expected Format -/// -/// ```json -/// { -/// "Devices": [ -/// {"DevicePath": "/dev/nvme0n1", "SerialNumber": "...", ...} -/// ] -/// } -/// ``` -/// -/// # References -/// -/// - [nvme-cli](https://github.com/linux-nvme/nvme-cli) -pub fn parse_nvme_list_json(output: &str) -> Result, String> { - todo!() -} - -/// Parse smartctl JSON output -/// -/// # Arguments -/// -/// * `output` - JSON output from `smartctl -i --json /dev/{device}` -/// -/// # Returns -/// -/// Partial storage device with SMART information. -/// -/// # References -/// -/// - [smartmontools](https://www.smartmontools.org/) -pub fn parse_smartctl_json(output: &str) -> Result { - todo!() -} - -/// Check if device name is a virtual device -/// -/// # Arguments -/// -/// * `name` - Block device name (e.g., "sda", "loop0", "dm-0") -/// -/// # Returns -/// -/// `true` if the device is virtual (loop, ram, dm-*, etc.) -pub fn is_virtual_device(name: &str) -> bool { - name.starts_with("loop") - || name.starts_with("ram") - || name.starts_with("dm-") - || name.starts_with("zram") - || name.starts_with("nbd") -} -``` - ---- - -## ARM/aarch64 Considerations - -### Known ARM Platforms - -| Platform | Storage Type | Notes | -|----------|--------------|-------| -| NVIDIA DGX Spark | NVMe | Grace Hopper, ARM Neoverse | -| AWS Graviton | NVMe, EBS | Various instance storage | -| Ampere Altra | NVMe | Server-class ARM | -| Raspberry Pi | SD/eMMC | mmcblk* devices | -| Apple Silicon | NVMe | Not Linux target | - -### ARM-Specific sysfs Paths - -Some ARM platforms use slightly different sysfs layouts: - -``` -# Standard path -/sys/block/nvme0n1/device/serial - -# Some ARM platforms -/sys/class/nvme/nvme0/serial - -# eMMC on ARM -/sys/block/mmcblk0/device/cid # Contains serial in CID register -``` - -### eMMC CID Parsing - -eMMC devices encode serial number in the CID (Card Identification) register: - -```rust -/// Parse eMMC CID to extract serial number -/// -/// # Arguments -/// -/// * `cid` - Content of `/sys/block/mmcblk*/device/cid` (32 hex chars) -/// -/// # References -/// -/// - [JEDEC eMMC Standard](https://www.jedec.org/) -pub fn parse_emmc_cid_serial(cid: &str) -> Option { - // CID format: MID(1) + OID(2) + PNM(6) + PRV(1) + PSN(4) + MDT(2) + CRC(1) - // PSN (Product Serial Number) is bytes 10-13 - if cid.len() < 32 { - return None; - } - let serial_hex = &cid[20..28]; // PSN bytes - Some(serial_hex.to_uppercase()) -} -``` - -### Testing on ARM - -```bash -# Test sysfs availability -ls -la /sys/block/ -cat /sys/block/*/size - -# Check for NVMe -ls -la /sys/class/nvme/ - -# Check for eMMC -ls -la /sys/block/mmcblk* -``` - ---- - -## Testing Requirements - -### Unit Tests - -| Test | Description | -|------|-------------| -| `test_parse_sysfs_size` | Parse sector count to bytes | -| `test_parse_sysfs_rotational` | Parse rotational flag | -| `test_parse_lsblk_json` | Parse lsblk JSON output | -| `test_parse_nvme_list_json` | Parse nvme-cli JSON | -| `test_parse_smartctl_json` | Parse smartctl JSON | -| `test_storage_type_from_device` | Device name to type mapping | -| `test_is_virtual_device` | Virtual device detection | -| `test_parse_emmc_cid` | eMMC CID serial extraction | - -### Integration Tests - -| Test | Platform | Description | -|------|----------|-------------| -| `test_sysfs_detection` | Linux | Full sysfs detection | -| `test_lsblk_detection` | Linux | lsblk fallback | -| `test_nvme_detection` | Linux + NVMe | NVMe-specific detection | -| `test_arm_detection` | aarch64 | ARM platform detection | -| `test_emmc_detection` | aarch64 | eMMC device detection | - -### Test Hardware Matrix - -| Platform | Storage | Test Type | -|----------|---------|-----------| -| x86_64 Linux | NVMe | CI + Manual | -| x86_64 Linux | SATA SSD | Manual | -| x86_64 Linux | HDD | Manual | -| aarch64 Linux (DGX Spark) | NVMe | Manual | -| aarch64 Linux (Graviton) | NVMe | CI | -| aarch64 Linux (RPi) | eMMC | Manual | - ---- - -## References - -### Official Documentation - -| Resource | URL | -|----------|-----| -| Linux sysfs block ABI | https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-block | -| Linux sysfs nvme ABI | https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-nvme | -| NVMe Specification | https://nvmexpress.org/specifications/ | -| JEDEC eMMC Standard | https://www.jedec.org/ | -| smartmontools | https://www.smartmontools.org/ | -| nvme-cli | https://github.com/linux-nvme/nvme-cli | -| lsblk (util-linux) | https://github.com/util-linux/util-linux | - -### Crate Documentation - -| Crate | URL | -|-------|-----| -| sysinfo | https://docs.rs/sysinfo | -| serde_json | https://docs.rs/serde_json | - -### Kernel Documentation - -| Path | Description | -|------|-------------| -| `/sys/block/` | Block device sysfs | -| `/sys/class/nvme/` | NVMe controller class | -| `/proc/partitions` | Partition information | -| `/dev/disk/by-id/` | Persistent device naming | - ---- - -## Changelog - -| Date | Changes | -|------|---------| -| 2024-12-29 | Initial specification | diff --git a/docs/TESTING_STRATEGY.md b/docs/TESTING_STRATEGY.md deleted file mode 100644 index 396dd6d..0000000 --- a/docs/TESTING_STRATEGY.md +++ /dev/null @@ -1,709 +0,0 @@ -# Testing Strategy - -> **Target Platforms:** Linux x86_64, Linux aarch64 -> **Primary Test Target:** aarch64 (ARM64) - DGX Spark, Graviton - -## Table of Contents - -1. [Overview](#overview) -2. [Test Categories](#test-categories) -3. [Platform Matrix](#platform-matrix) -4. [Unit Testing](#unit-testing) -5. [Integration Testing](#integration-testing) -6. [Hardware Testing](#hardware-testing) -7. [CI/CD Configuration](#cicd-configuration) -8. [Test Data Management](#test-data-management) -9. [Mocking Strategy](#mocking-strategy) - ---- - -## Overview - -The `hardware_report` crate requires testing across multiple architectures and hardware configurations. This document defines the testing strategy to ensure reliability on both x86_64 and aarch64 platforms. - -### Testing Principles - -1. **Parser functions are pure** - Test with captured output, no hardware needed -2. **Adapters require mocking** - Use trait-based dependency injection -3. **Integration tests need real hardware** - Run on CI matrix or manual -4. **ARM is primary target** - Ensure full coverage on aarch64 - ---- - -## Test Categories - -### Test Pyramid - -``` - ┌─────────────────┐ - │ Manual/E2E │ ← Real hardware, manual verification - │ (5%) │ - ├─────────────────┤ - │ Integration │ ← Real sysfs, commands, CI matrix - │ (20%) │ - ├─────────────────┤ - │ Unit Tests │ ← Pure functions, mocked adapters - │ (75%) │ - └─────────────────┘ -``` - -### Test Types - -| Type | Location | Dependencies | CI | -|------|----------|--------------|-----| -| Unit | `src/**/*.rs` (inline) | None | Yes | -| Parser | `tests/parsers/` | Sample data files | Yes | -| Adapter | `tests/adapters/` | Mocked traits | Yes | -| Integration | `tests/integration/` | Real system | Matrix | -| Hardware | `tests/hardware/` | Physical hardware | Manual | - ---- - -## Platform Matrix - -### Target Platforms - -| Platform | Architecture | GPU | CI Support | Notes | -|----------|--------------|-----|------------|-------| -| Linux x86_64 | x86_64 | NVIDIA | GitHub Actions | Standard runners | -| Linux x86_64 | x86_64 | AMD | Self-hosted | Optional | -| Linux aarch64 | aarch64 | None | GitHub Actions | `ubuntu-24.04-arm` | -| Linux aarch64 | aarch64 | NVIDIA | Self-hosted | DGX Spark | -| macOS x86_64 | x86_64 | Apple | GitHub Actions | Legacy Intel | -| macOS aarch64 | aarch64 | Apple | GitHub Actions | M1/M2/M3 | - -### CI Matrix Configuration - -```yaml -strategy: - matrix: - include: - # x86_64 Linux - - os: ubuntu-latest - arch: x86_64 - target: x86_64-unknown-linux-gnu - features: "full" - - # aarch64 Linux (GitHub-hosted ARM) - - os: ubuntu-24.04-arm - arch: aarch64 - target: aarch64-unknown-linux-gnu - features: "" # No nvidia feature on ARM CI - - # Cross-compile for ARM (build only) - - os: ubuntu-latest - arch: x86_64 - target: aarch64-unknown-linux-gnu - cross: true - features: "" -``` - ---- - -## Unit Testing - -### Parser Unit Tests - -Parser functions are pure and easily testable: - -```rust -// src/domain/parsers/storage.rs - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_sysfs_size() { - // 2TB drive in 512-byte sectors - assert_eq!(parse_sysfs_size("3907029168").unwrap(), 2000398934016); - - // 1TB drive - assert_eq!(parse_sysfs_size("1953525168").unwrap(), 1000204886016); - - // Empty/whitespace - assert!(parse_sysfs_size("").is_err()); - assert!(parse_sysfs_size(" ").is_err()); - } - - #[test] - fn test_parse_sysfs_rotational() { - assert!(parse_sysfs_rotational("1")); // HDD - assert!(!parse_sysfs_rotational("0")); // SSD - assert!(!parse_sysfs_rotational("0\n")); // With newline - } - - #[test] - fn test_storage_type_from_device() { - assert_eq!(StorageType::from_device("nvme0n1", false), StorageType::Nvme); - assert_eq!(StorageType::from_device("sda", false), StorageType::Ssd); - assert_eq!(StorageType::from_device("sda", true), StorageType::Hdd); - assert_eq!(StorageType::from_device("mmcblk0", false), StorageType::Emmc); - assert_eq!(StorageType::from_device("loop0", false), StorageType::Virtual); - } -} -``` - -### GPU Parser Tests - -```rust -// src/domain/parsers/gpu.rs - -#[cfg(test)] -mod tests { - use super::*; - - const NVIDIA_SMI_OUTPUT: &str = r#"0, NVIDIA H100 80GB HBM3, GPU-12345678-1234-1234-1234-123456789abc, 81920, 81000, 00000000:01:00.0, 535.129.03, 9.0 -1, NVIDIA H100 80GB HBM3, GPU-87654321-4321-4321-4321-cba987654321, 81920, 80500, 00000000:02:00.0, 535.129.03, 9.0"#; - - #[test] - fn test_parse_nvidia_smi_output() { - let gpus = parse_nvidia_smi_output(NVIDIA_SMI_OUTPUT).unwrap(); - - assert_eq!(gpus.len(), 2); - - assert_eq!(gpus[0].index, 0); - assert_eq!(gpus[0].name, "NVIDIA H100 80GB HBM3"); - assert_eq!(gpus[0].memory_total_mb, 81920); - assert_eq!(gpus[0].memory_free_mb, Some(81000)); - assert_eq!(gpus[0].driver_version, Some("535.129.03".to_string())); - assert_eq!(gpus[0].compute_capability, Some("9.0".to_string())); - } - - #[test] - fn test_parse_nvidia_smi_empty() { - let gpus = parse_nvidia_smi_output("").unwrap(); - assert!(gpus.is_empty()); - } - - #[test] - fn test_parse_pci_vendor() { - assert_eq!(parse_pci_vendor("10de"), GpuVendor::Nvidia); - assert_eq!(parse_pci_vendor("0x10de"), GpuVendor::Nvidia); - assert_eq!(parse_pci_vendor("1002"), GpuVendor::Amd); - assert_eq!(parse_pci_vendor("8086"), GpuVendor::Intel); - assert_eq!(parse_pci_vendor("abcd"), GpuVendor::Unknown); - } -} -``` - -### CPU Parser Tests - -```rust -// src/domain/parsers/cpu.rs - -#[cfg(test)] -mod tests { - use super::*; - - const PROC_CPUINFO_INTEL: &str = r#" -processor : 0 -vendor_id : GenuineIntel -cpu family : 6 -model : 106 -model name : Intel(R) Xeon(R) Platinum 8380 CPU @ 2.30GHz -stepping : 6 -microcode : 0xd0003a5 -flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat avx avx2 avx512f avx512dq -"#; - - const PROC_CPUINFO_ARM: &str = r#" -processor : 0 -BogoMIPS : 50.00 -Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp -CPU implementer : 0x41 -CPU architecture: 8 -CPU variant : 0x3 -CPU part : 0xd0c -CPU revision : 1 -"#; - - #[test] - fn test_parse_proc_cpuinfo_intel() { - let info = parse_proc_cpuinfo(PROC_CPUINFO_INTEL).unwrap(); - - assert_eq!(info.vendor, "GenuineIntel"); - assert_eq!(info.model, "Intel(R) Xeon(R) Platinum 8380 CPU @ 2.30GHz"); - assert_eq!(info.family, Some(6)); - assert_eq!(info.model_number, Some(106)); - assert!(info.flags.contains(&"avx512f".to_string())); - } - - #[test] - fn test_parse_proc_cpuinfo_arm() { - let info = parse_proc_cpuinfo(PROC_CPUINFO_ARM).unwrap(); - - assert_eq!(info.vendor, "ARM"); - assert!(info.flags.contains(&"asimd".to_string())); - assert_eq!(info.microarchitecture, Some("Neoverse N1".to_string())); - } - - #[test] - fn test_arm_cpu_part_mapping() { - assert_eq!(arm_cpu_part_to_name("0xd0c"), Some("Neoverse N1")); - assert_eq!(arm_cpu_part_to_name("0xd40"), Some("Neoverse V1")); - assert_eq!(arm_cpu_part_to_name("0xd49"), Some("Neoverse N2")); - assert_eq!(arm_cpu_part_to_name("0xffff"), None); - } - - #[test] - fn test_parse_sysfs_freq() { - assert_eq!(parse_sysfs_freq_khz("3500000").unwrap(), 3500); - assert_eq!(parse_sysfs_freq_khz("2100000\n").unwrap(), 2100); - assert!(parse_sysfs_freq_khz("invalid").is_err()); - } - - #[test] - fn test_parse_cache_size() { - assert_eq!(parse_sysfs_cache_size("32K").unwrap(), 32); - assert_eq!(parse_sysfs_cache_size("1M").unwrap(), 1024); - assert_eq!(parse_sysfs_cache_size("256M").unwrap(), 262144); - assert_eq!(parse_sysfs_cache_size("32768K").unwrap(), 32768); - } -} -``` - ---- - -## Integration Testing - -### sysfs Integration Tests - -```rust -// tests/integration/sysfs_storage.rs - -#[cfg(target_os = "linux")] -mod tests { - use std::fs; - use std::path::Path; - - #[test] - fn test_sysfs_block_exists() { - assert!(Path::new("/sys/block").exists()); - } - - #[test] - fn test_can_read_block_devices() { - let entries = fs::read_dir("/sys/block").unwrap(); - let devices: Vec<_> = entries - .filter_map(|e| e.ok()) - .map(|e| e.file_name().to_string_lossy().to_string()) - .filter(|n| !n.starts_with("loop") && !n.starts_with("ram")) - .collect(); - - // Most systems have at least one real block device - // This may fail in minimal containers - println!("Found block devices: {:?}", devices); - } - - #[test] - fn test_can_read_device_size() { - if let Ok(entries) = fs::read_dir("/sys/block") { - for entry in entries.flatten() { - let name = entry.file_name(); - let size_path = format!("/sys/block/{}/size", name.to_string_lossy()); - - if let Ok(size_str) = fs::read_to_string(&size_path) { - let sectors: u64 = size_str.trim().parse().unwrap_or(0); - let bytes = sectors * 512; - println!("{}: {} bytes", name.to_string_lossy(), bytes); - } - } - } - } -} -``` - -### Command Execution Tests - -```rust -// tests/integration/commands.rs - -#[cfg(target_os = "linux")] -mod tests { - use std::process::Command; - - #[test] - fn test_lsblk_available() { - let output = Command::new("which").arg("lsblk").output(); - assert!(output.is_ok()); - } - - #[test] - fn test_lsblk_json_output() { - let output = Command::new("lsblk") - .args(["-J", "-o", "NAME,SIZE,TYPE"]) - .output(); - - match output { - Ok(out) if out.status.success() => { - let stdout = String::from_utf8_lossy(&out.stdout); - assert!(stdout.contains("blockdevices")); - } - _ => { - // lsblk may not be available in all environments - println!("lsblk not available, skipping"); - } - } - } - - #[test] - #[cfg(target_arch = "aarch64")] - fn test_arm_specific_detection() { - // Verify ARM-specific paths exist - let cpuinfo = std::fs::read_to_string("/proc/cpuinfo").unwrap(); - - // ARM cpuinfo has different format - assert!( - cpuinfo.contains("CPU implementer") || - cpuinfo.contains("model name"), - "Expected ARM or x86 CPU info format" - ); - } -} -``` - ---- - -## Hardware Testing - -### Manual Test Checklist - -#### Storage Tests - -```bash -# Run on target hardware -cargo test --test storage_hardware -- --ignored - -# Expected output validation: -# - At least one storage device detected -# - Size > 0 for all devices -# - Type correctly identified (NVMe/SSD/HDD) -# - Serial number present (may need sudo) -``` - -#### GPU Tests - -```bash -# Run on NVIDIA system -cargo test --test gpu_hardware --features nvidia -- --ignored - -# Expected output validation: -# - All GPUs detected -# - Memory matches nvidia-smi -# - Driver version present -# - PCI bus ID present -``` - -### Hardware Test Files - -```rust -// tests/hardware/storage.rs - -#[test] -#[ignore] // Run manually on real hardware -fn test_storage_detection_real_hardware() { - let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(async { - let service = hardware_report::create_service().unwrap(); - let config = hardware_report::ReportConfig::default(); - let report = service.generate_report(config).await.unwrap(); - - // Validate storage - assert!(!report.hardware.storage.devices.is_empty(), - "No storage devices detected"); - - for device in &report.hardware.storage.devices { - assert!(device.size_bytes > 0, - "Device {} has zero size", device.name); - assert!(!device.model.is_empty(), - "Device {} has empty model", device.name); - println!("Found: {} - {} - {} GB", - device.name, device.model, device.size_gb); - } - }); -} - -#[test] -#[ignore] -#[cfg(target_arch = "aarch64")] -fn test_arm_storage_detection() { - // ARM-specific storage test - let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(async { - let service = hardware_report::create_service().unwrap(); - let config = hardware_report::ReportConfig::default(); - let report = service.generate_report(config).await.unwrap(); - - // On ARM, we should detect NVMe or eMMC - assert!(!report.hardware.storage.devices.is_empty(), - "No storage on ARM - sysfs fallback may have failed"); - - let has_nvme = report.hardware.storage.devices.iter() - .any(|d| d.device_type == StorageType::Nvme); - let has_emmc = report.hardware.storage.devices.iter() - .any(|d| d.device_type == StorageType::Emmc); - - println!("ARM storage: NVMe={}, eMMC={}", has_nvme, has_emmc); - }); -} -``` - ---- - -## CI/CD Configuration - -### GitHub Actions Workflow - -```yaml -# .github/workflows/test.yml - -name: Test - -on: - push: - branches: [main] - pull_request: - branches: [main] - -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: 1 - -jobs: - test-x86: - name: Test x86_64 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-action@stable - - - name: Run unit tests - run: cargo test --lib --all-features - - - name: Run doc tests - run: cargo test --doc - - - name: Run integration tests - run: cargo test --test '*' -- --skip hardware - - test-arm: - name: Test aarch64 - runs-on: ubuntu-24.04-arm - steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-action@stable - - - name: Run unit tests - run: cargo test --lib - - - name: Run ARM integration tests - run: cargo test --test '*' -- --skip hardware - - - name: Test ARM-specific code paths - run: | - # Verify ARM detection works - cargo run --example basic_usage 2>&1 | tee output.txt - grep -q "architecture.*aarch64" output.txt || echo "Warning: arch detection may have issues" - - cross-compile: - name: Cross-compile check - runs-on: ubuntu-latest - strategy: - matrix: - target: - - aarch64-unknown-linux-gnu - - aarch64-unknown-linux-musl - steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-action@stable - with: - targets: ${{ matrix.target }} - - - name: Install cross - run: cargo install cross - - - name: Cross build - run: cross build --target ${{ matrix.target }} --release - - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-action@stable - with: - components: clippy, rustfmt - - - name: Check formatting - run: cargo fmt --check - - - name: Clippy - run: cargo clippy --all-features -- -D warnings - - - name: Check docs - run: cargo doc --no-deps --all-features - env: - RUSTDOCFLAGS: -D warnings -``` - ---- - -## Test Data Management - -### Sample Data Files - -Store captured command outputs for parser testing: - -``` -tests/ -├── data/ -│ ├── nvidia-smi/ -│ │ ├── h100-8gpu.csv -│ │ ├── a100-4gpu.csv -│ │ └── no-gpu.csv -│ ├── lsblk/ -│ │ ├── nvme-only.json -│ │ ├── mixed-storage.json -│ │ └── arm-emmc.json -│ ├── proc/ -│ │ ├── cpuinfo-intel-xeon.txt -│ │ ├── cpuinfo-amd-epyc.txt -│ │ └── cpuinfo-arm-neoverse.txt -│ └── sysfs/ -│ ├── block-nvme/ -│ └── cpu-arm/ -``` - -### Loading Test Data - -```rust -// tests/common/mod.rs - -use std::path::PathBuf; - -pub fn test_data_path(relative: &str) -> PathBuf { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("tests/data"); - path.push(relative); - path -} - -pub fn load_test_data(relative: &str) -> String { - std::fs::read_to_string(test_data_path(relative)) - .expect(&format!("Failed to load test data: {}", relative)) -} - -// Usage in tests: -#[test] -fn test_nvidia_h100_parsing() { - let data = load_test_data("nvidia-smi/h100-8gpu.csv"); - let gpus = parse_nvidia_smi_output(&data).unwrap(); - assert_eq!(gpus.len(), 8); -} -``` - ---- - -## Mocking Strategy - -### Trait-Based Mocking - -```rust -// src/ports/secondary/system.rs - The trait - -#[async_trait] -pub trait SystemInfoProvider: Send + Sync { - async fn get_storage_info(&self) -> Result; - async fn get_gpu_info(&self) -> Result; - // ... -} - -// tests/mocks/system.rs - Mock implementation - -pub struct MockSystemInfoProvider { - pub storage_result: Result, - pub gpu_result: Result, -} - -#[async_trait] -impl SystemInfoProvider for MockSystemInfoProvider { - async fn get_storage_info(&self) -> Result { - self.storage_result.clone() - } - - async fn get_gpu_info(&self) -> Result { - self.gpu_result.clone() - } -} - -// Usage in tests -#[tokio::test] -async fn test_service_with_mock() { - let mock = MockSystemInfoProvider { - storage_result: Ok(StorageInfo { - devices: vec![ - StorageDevice { - name: "nvme0n1".to_string(), - size_bytes: 1000204886016, - ..Default::default() - } - ] - }), - gpu_result: Ok(GpuInfo { devices: vec![] }), - }; - - // Inject mock into service - let service = HardwareCollectionService::new(Arc::new(mock)); - let report = service.generate_report(Default::default()).await.unwrap(); - - assert_eq!(report.hardware.storage.devices.len(), 1); -} -``` - -### Command Executor Mocking - -```rust -// Mock command executor for testing adapter logic - -pub struct MockCommandExecutor { - pub responses: HashMap, -} - -impl MockCommandExecutor { - pub fn new() -> Self { - Self { responses: HashMap::new() } - } - - pub fn mock_command(&mut self, cmd: &str, result: CommandResult) { - self.responses.insert(cmd.to_string(), result); - } -} - -#[async_trait] -impl CommandExecutor for MockCommandExecutor { - async fn execute(&self, cmd: &SystemCommand) -> Result { - if let Some(result) = self.responses.get(&cmd.program) { - Ok(result.clone()) - } else { - Err(CommandError::NotFound(cmd.program.clone())) - } - } -} -``` - ---- - -## Changelog - -| Date | Changes | -|------|---------| -| 2024-12-29 | Initial testing strategy | From 1b2516842a93d9e175bd7fd3a6b38a7c30792894 Mon Sep 17 00:00:00 2001 From: "Kenny (Knight) Sheridan" Date: Mon, 5 Jan 2026 16:48:27 -0800 Subject: [PATCH 7/8] fix: apply cargo fmt formatting --- src/adapters/secondary/system/linux.rs | 81 +++++++++++--------------- src/domain/errors.rs | 9 +-- src/domain/parsers/cpu.rs | 8 +-- src/domain/parsers/gpu.rs | 21 ++++--- src/domain/parsers/storage.rs | 9 +-- 5 files changed, 56 insertions(+), 72 deletions(-) diff --git a/src/adapters/secondary/system/linux.rs b/src/adapters/secondary/system/linux.rs index 7270b9e..a8704b7 100644 --- a/src/adapters/secondary/system/linux.rs +++ b/src/adapters/secondary/system/linux.rs @@ -34,35 +34,12 @@ limitations under the License. //! 3. Fallback: sysinfo crate (cross-platform) use crate::domain::{ - combine_cpu_info, - determine_memory_speed, - determine_memory_type, - parse_dmidecode_bios_info, - parse_dmidecode_chassis_info, - parse_dmidecode_cpu, - parse_dmidecode_memory, - parse_dmidecode_system_info, - parse_free_output, - parse_hostname_output, - parse_ip_output, - parse_lscpu_output, - BiosInfo, - ChassisInfo, - CpuInfo, - GpuDevice, - GpuInfo, - GpuVendor, - MemoryInfo, - MotherboardInfo, - NetworkInfo, - NetworkInterface, - NetworkInterfaceType, - NumaNode, - StorageDevice, - StorageInfo, - StorageType, - SystemError, - SystemInfo, + combine_cpu_info, determine_memory_speed, determine_memory_type, parse_dmidecode_bios_info, + parse_dmidecode_chassis_info, parse_dmidecode_cpu, parse_dmidecode_memory, + parse_dmidecode_system_info, parse_free_output, parse_hostname_output, parse_ip_output, + parse_lscpu_output, BiosInfo, ChassisInfo, CpuInfo, GpuDevice, GpuInfo, GpuVendor, MemoryInfo, + MotherboardInfo, NetworkInfo, NetworkInterface, NetworkInterfaceType, NumaNode, StorageDevice, + StorageInfo, StorageType, SystemError, SystemInfo, }; use crate::domain::parsers::storage::{ @@ -156,7 +133,11 @@ impl LinuxSystemInfoProvider { // Skip tiny devices (< 1GB) const MIN_SIZE: u64 = 1_000_000_000; if size_bytes < MIN_SIZE { - log::trace!("Skipping small device {}: {} bytes", device_name, size_bytes); + log::trace!( + "Skipping small device {}: {} bytes", + device_name, + size_bytes + ); continue; } @@ -273,13 +254,15 @@ impl LinuxSystemInfoProvider { ]) .timeout(Duration::from_secs(10)); - let output = self.command_executor.execute(&cmd).await.map_err(|e| { - SystemError::CommandFailed { - command: "lsblk".to_string(), - exit_code: None, - stderr: e.to_string(), - } - })?; + let output = + self.command_executor + .execute(&cmd) + .await + .map_err(|e| SystemError::CommandFailed { + command: "lsblk".to_string(), + exit_code: None, + stderr: e.to_string(), + })?; if !output.success { return Err(SystemError::CommandFailed { @@ -331,8 +314,12 @@ impl LinuxSystemInfoProvider { for sec_device in secondary { if let Some(pri_device) = primary.iter_mut().find(|d| d.name == sec_device.name) { // Fill in missing fields from secondary - pri_device.serial_number = pri_device.serial_number.take().or(sec_device.serial_number); - pri_device.firmware_version = pri_device.firmware_version.take().or(sec_device.firmware_version); + pri_device.serial_number = + pri_device.serial_number.take().or(sec_device.serial_number); + pri_device.firmware_version = pri_device + .firmware_version + .take() + .or(sec_device.firmware_version); pri_device.wwn = pri_device.wwn.take().or(sec_device.wwn); if pri_device.model.is_empty() && !sec_device.model.is_empty() { @@ -400,8 +387,9 @@ impl LinuxSystemInfoProvider { iface.driver = Some(driver_str.clone()); // Driver version - let version_path = - PathBuf::from("/sys/module").join(&driver_str).join("version"); + let version_path = PathBuf::from("/sys/module") + .join(&driver_str) + .join("version"); if let Ok(version) = self.read_sysfs_file(&version_path) { iface.driver_version = Some(version.trim().to_string()); } @@ -527,7 +515,10 @@ impl SystemInfoProvider for LinuxSystemInfoProvider { // Enrich with lsblk if let Ok(lsblk_devices) = self.detect_storage_lsblk().await { - log::debug!("lsblk found {} devices for additional info", lsblk_devices.len()); + log::debug!( + "lsblk found {} devices for additional info", + lsblk_devices.len() + ); self.merge_storage_info(&mut devices, lsblk_devices); } @@ -597,8 +588,7 @@ impl SystemInfoProvider for LinuxSystemInfoProvider { if lspci_output.success { let mut gpu_index = 0; for line in lspci_output.stdout.lines() { - if line.to_lowercase().contains("vga") - || line.to_lowercase().contains("3d") + if line.to_lowercase().contains("vga") || line.to_lowercase().contains("3d") { devices.push(GpuDevice { index: gpu_index, @@ -634,8 +624,7 @@ impl SystemInfoProvider for LinuxSystemInfoProvider { } })?; - let mut interfaces = - parse_ip_output(&ip_output.stdout).map_err(SystemError::ParseError)?; + let mut interfaces = parse_ip_output(&ip_output.stdout).map_err(SystemError::ParseError)?; // Enrich with sysfs data for iface in &mut interfaces { diff --git a/src/domain/errors.rs b/src/domain/errors.rs index 10aa1aa..4e361aa 100644 --- a/src/domain/errors.rs +++ b/src/domain/errors.rs @@ -142,18 +142,13 @@ pub enum SystemError { /// I/O operation failed (simple) IoError(String), /// I/O operation failed (with path context) - IoErrorWithPath { - path: String, - message: String, - }, + IoErrorWithPath { path: String, message: String }, /// Parsing error ParseError(String), /// Timeout Timeout(String), /// Resource not available - NotAvailable { - resource: String, - }, + NotAvailable { resource: String }, } impl fmt::Display for SystemError { diff --git a/src/domain/parsers/cpu.rs b/src/domain/parsers/cpu.rs index 591d292..4e221a4 100644 --- a/src/domain/parsers/cpu.rs +++ b/src/domain/parsers/cpu.rs @@ -54,7 +54,7 @@ pub fn parse_sysfs_freq_khz(content: &str) -> Result { /// Size in KB. pub fn parse_sysfs_cache_size(content: &str) -> Result { let trimmed = content.trim(); - + if trimmed.ends_with('K') || trimmed.ends_with('k') { let value: u32 = trimmed[..trimmed.len() - 1] .parse() @@ -83,12 +83,12 @@ pub fn parse_sysfs_cache_size(content: &str) -> Result { /// * `content` - Content of /proc/cpuinfo pub fn parse_proc_cpuinfo(content: &str) -> Result { let mut cpu_info = CpuInfo::default(); - + for line in content.lines() { if let Some((key, value)) = line.split_once(':') { let key = key.trim(); let value = value.trim(); - + match key { "vendor_id" => cpu_info.vendor = value.to_string(), "model name" => { @@ -115,7 +115,7 @@ pub fn parse_proc_cpuinfo(content: &str) -> Result { } } } - + Ok(cpu_info) } diff --git a/src/domain/parsers/gpu.rs b/src/domain/parsers/gpu.rs index 1447610..93e4dd3 100644 --- a/src/domain/parsers/gpu.rs +++ b/src/domain/parsers/gpu.rs @@ -108,7 +108,7 @@ pub fn parse_lspci_gpu_output(output: &str) -> Result, String> { for line in output.lines() { let line_lower = line.to_lowercase(); - + // Look for VGA compatible or 3D controller if !line_lower.contains("vga") && !line_lower.contains("3d") { continue; @@ -116,7 +116,7 @@ pub fn parse_lspci_gpu_output(output: &str) -> Result, String> { // Extract PCI ID from brackets like [10de:2204] let pci_id = extract_pci_id(line); - + // Determine vendor from PCI ID let (vendor_enum, vendor_name) = if let Some(ref pci) = pci_id { let vendor_id = pci.split(':').next().unwrap_or(""); @@ -161,13 +161,13 @@ fn extract_pci_id(line: &str) -> Option { let abs_start = search_start + start; if let Some(end) = line[abs_start..].find(']') { let bracket_content = &line[abs_start + 1..abs_start + end]; - + // Check if it looks like a PCI ID (4 hex chars : 4 hex chars) if bracket_content.len() == 9 && bracket_content.chars().nth(4) == Some(':') { // Verify it's all hex chars let parts: Vec<&str> = bracket_content.split(':').collect(); - if parts.len() == 2 - && parts[0].len() == 4 + if parts.len() == 2 + && parts[0].len() == 4 && parts[1].len() == 4 && parts[0].chars().all(|c| c.is_ascii_hexdigit()) && parts[1].chars().all(|c| c.is_ascii_hexdigit()) @@ -191,7 +191,7 @@ mod tests { fn test_parse_nvidia_smi_output() { let output = "0, NVIDIA GeForce RTX 3090, GPU-12345678-1234-1234-1234-123456789012, 24576, 24000, 00000000:01:00.0, 535.129.03, 8.6"; let devices = parse_nvidia_smi_output(output).unwrap(); - + assert_eq!(devices.len(), 1); assert_eq!(devices[0].name, "NVIDIA GeForce RTX 3090"); assert_eq!(devices[0].memory_total_mb, 24576); @@ -202,9 +202,9 @@ mod tests { fn test_parse_lspci_gpu_output() { let output = r#"01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA102 [GeForce RTX 3090] [10de:2204] (rev a1) 00:02.0 VGA compatible controller [0300]: Intel Corporation Device [8086:9a49] (rev 01)"#; - + let devices = parse_lspci_gpu_output(output).unwrap(); - + assert_eq!(devices.len(), 2); assert_eq!(devices[0].vendor, "NVIDIA"); assert_eq!(devices[1].vendor, "Intel"); @@ -213,7 +213,10 @@ mod tests { #[test] fn test_extract_pci_id() { assert_eq!(extract_pci_id("[10de:2204]"), Some("10de:2204".to_string())); - assert_eq!(extract_pci_id("NVIDIA [10de:2204] (rev a1)"), Some("10de:2204".to_string())); + assert_eq!( + extract_pci_id("NVIDIA [10de:2204] (rev a1)"), + Some("10de:2204".to_string()) + ); assert_eq!(extract_pci_id("No PCI ID here"), None); } } diff --git a/src/domain/parsers/storage.rs b/src/domain/parsers/storage.rs index 11dd86e..a2d7998 100644 --- a/src/domain/parsers/storage.rs +++ b/src/domain/parsers/storage.rs @@ -75,8 +75,8 @@ pub fn is_virtual_device(name: &str) -> bool { /// /// * `output` - JSON output from `lsblk -J -b -d -o NAME,SIZE,TYPE,MODEL,SERIAL,ROTA,TRAN,WWN` pub fn parse_lsblk_json(output: &str) -> Result, String> { - let json: serde_json::Value = serde_json::from_str(output) - .map_err(|e| format!("Failed to parse lsblk JSON: {}", e))?; + let json: serde_json::Value = + serde_json::from_str(output).map_err(|e| format!("Failed to parse lsblk JSON: {}", e))?; let blockdevices = json .get("blockdevices") @@ -97,10 +97,7 @@ pub fn parse_lsblk_json(output: &str) -> Result, String> { continue; } - let size_bytes = device - .get("size") - .and_then(|v| v.as_u64()) - .unwrap_or(0); + let size_bytes = device.get("size").and_then(|v| v.as_u64()).unwrap_or(0); // Skip small devices if size_bytes < 1_000_000_000 { From 6b6f7748c7aa4bba23fe8f878c9066ab17a2d6f9 Mon Sep 17 00:00:00 2001 From: "Kenny (Knight) Sheridan" Date: Mon, 5 Jan 2026 16:52:22 -0800 Subject: [PATCH 8/8] fix: resolve clippy warnings - use derive macros and strip_prefix --- src/adapters/secondary/system/linux.rs | 4 +- src/domain/entities.rs | 54 ++++---------------------- 2 files changed, 9 insertions(+), 49 deletions(-) diff --git a/src/adapters/secondary/system/linux.rs b/src/adapters/secondary/system/linux.rs index a8704b7..9915705 100644 --- a/src/adapters/secondary/system/linux.rs +++ b/src/adapters/secondary/system/linux.rs @@ -213,8 +213,8 @@ impl LinuxSystemInfoProvider { existing_firmware: Option, ) -> (Option, Option) { // Extract controller name: "nvme0n1" -> "nvme0" - let controller = if device_name.starts_with("nvme") { - if let Some(pos) = device_name[4..].find('n') { + let controller = if let Some(stripped) = device_name.strip_prefix("nvme") { + if let Some(pos) = stripped.find('n') { &device_name[..4 + pos] } else { device_name diff --git a/src/domain/entities.rs b/src/domain/entities.rs index e0b5d91..bd15e32 100644 --- a/src/domain/entities.rs +++ b/src/domain/entities.rs @@ -160,7 +160,7 @@ pub struct HardwareInfo { } /// CPU information -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct CpuInfo { /// CPU model name pub model: String, @@ -213,31 +213,6 @@ pub struct CpuInfo { pub detection_methods: Vec, } -impl Default for CpuInfo { - fn default() -> Self { - Self { - model: String::new(), - cores: 0, - threads: 0, - sockets: 0, - speed: String::new(), - vendor: String::new(), - architecture: String::new(), - frequency_mhz: 0, - frequency_min_mhz: None, - frequency_max_mhz: None, - cache_l1d_kb: None, - cache_l1i_kb: None, - cache_l2_kb: None, - cache_l3_kb: None, - flags: Vec::new(), - microarchitecture: None, - caches: Vec::new(), - detection_methods: Vec::new(), - } - } -} - impl CpuInfo { /// Set speed string from frequency_mhz pub fn set_speed_string(&mut self) { @@ -313,7 +288,7 @@ pub struct StorageInfo { } /// Storage type classification -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] pub enum StorageType { /// NVMe SSD Nvme, @@ -326,6 +301,7 @@ pub enum StorageType { /// Virtual device (should be filtered) Virtual, /// Unknown type + #[default] Unknown, } @@ -358,12 +334,6 @@ impl StorageType { } } -impl Default for StorageType { - fn default() -> Self { - StorageType::Unknown - } -} - /// Storage device information #[derive(Debug, Serialize, Deserialize, Clone)] pub struct StorageDevice { @@ -457,7 +427,7 @@ pub struct GpuInfo { } /// GPU vendor classification -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] pub enum GpuVendor { /// NVIDIA GPU Nvidia, @@ -468,6 +438,7 @@ pub enum GpuVendor { /// Apple GPU (Apple Silicon) Apple, /// Unknown vendor + #[default] Unknown, } @@ -494,12 +465,6 @@ impl GpuVendor { } } -impl Default for GpuVendor { - fn default() -> Self { - GpuVendor::Unknown - } -} - /// GPU device information #[derive(Debug, Serialize, Deserialize, Clone)] pub struct GpuDevice { @@ -584,7 +549,7 @@ pub struct NetworkInfo { } /// Network interface type classification -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] pub enum NetworkInterfaceType { /// Physical Ethernet interface Ethernet, @@ -607,15 +572,10 @@ pub enum NetworkInterfaceType { /// Macvlan interface Macvlan, /// Unknown type + #[default] Unknown, } -impl Default for NetworkInterfaceType { - fn default() -> Self { - NetworkInterfaceType::Unknown - } -} - /// Network interface information #[derive(Debug, Serialize, Deserialize, Clone)] pub struct NetworkInterface {