use serde::{Deserialize, Serialize}; use serde_json::Value; use std::fs; use std::path::Path; use std::process::Command; use sysinfo::System; #[derive(Serialize, Deserialize, Debug)] pub struct PhysicalHost { pub storage_drives: Vec, pub storage_controller: StorageController, pub memory_modules: Vec, pub cpus: Vec, pub chipset: Chipset, pub network_interfaces: Vec, pub management_interface: Option, pub host_uuid: String, } #[derive(Serialize, Deserialize, Debug)] pub struct StorageDrive { pub name: String, pub model: String, pub serial: String, pub size_bytes: u64, pub logical_block_size: u32, pub physical_block_size: u32, pub rotational: bool, pub wwn: Option, pub interface_type: String, pub smart_status: Option, } #[derive(Serialize, Deserialize, Debug)] pub struct StorageController { pub name: String, pub driver: String, } #[derive(Serialize, Deserialize, Debug)] pub struct MemoryModule { pub size_bytes: u64, pub speed_mhz: Option, pub manufacturer: Option, pub part_number: Option, pub serial_number: Option, pub rank: Option, } #[derive(Serialize, Deserialize, Debug)] pub struct CPU { pub model: String, pub vendor: String, pub cores: u32, pub threads: u32, pub frequency_mhz: u64, } #[derive(Serialize, Deserialize, Debug)] pub struct Chipset { pub name: String, pub vendor: String, } #[derive(Serialize, Deserialize, Debug)] pub struct NetworkInterface { pub name: String, pub mac_address: String, pub speed_mbps: Option, pub is_up: bool, pub mtu: u32, pub ipv4_addresses: Vec, pub ipv6_addresses: Vec, pub driver: String, pub firmware_version: Option, } #[derive(Serialize, Deserialize, Debug)] pub struct ManagementInterface { pub kind: String, pub address: Option, pub firmware: Option, } impl PhysicalHost { pub fn gather() -> Self { let mut sys = System::new_all(); sys.refresh_all(); Self { storage_drives: Self::gather_storage_drives(), storage_controller: Self::gather_storage_controller(), memory_modules: Self::gather_memory_modules(), cpus: Self::gather_cpus(&sys), chipset: Self::gather_chipset(), network_interfaces: Self::gather_network_interfaces(), management_interface: Self::gather_management_interface(), host_uuid: Self::get_host_uuid(), } } fn gather_storage_drives() -> Vec { let mut drives = Vec::new(); // Use lsblk with JSON output for robust parsing if let Ok(output) = Command::new("lsblk") .args([ "-d", "-o", "NAME,MODEL,SERIAL,SIZE,ROTA,WWN", "-n", "-e", "7", "--json", ]) .output() && output.status.success() && let Ok(json) = serde_json::from_slice::(&output.stdout) && let Some(blockdevices) = json.get("blockdevices").and_then(|v| v.as_array()) { for device in blockdevices { let name = device .get("name") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); if name.is_empty() { continue; } let model = device .get("model") .and_then(|v| v.as_str()) .map(|s| s.trim().to_string()) .unwrap_or_default(); let serial = device .get("serial") .and_then(|v| v.as_str()) .map(|s| s.trim().to_string()) .unwrap_or_default(); let size_str = device.get("size").and_then(|v| v.as_str()).unwrap_or("0"); let size_bytes = Self::parse_size(size_str).unwrap_or(0); let rotational = device .get("rota") .and_then(|v| v.as_bool()) .unwrap_or(false); let wwn = device .get("wwn") .and_then(|v| v.as_str()) .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty() && s != "null"); let device_path = Path::new("/sys/block").join(&name); let mut drive = StorageDrive { name: name.clone(), model, serial, size_bytes, logical_block_size: Self::read_sysfs_u32( &device_path.join("queue/logical_block_size"), ) .unwrap_or(512), physical_block_size: Self::read_sysfs_u32( &device_path.join("queue/physical_block_size"), ) .unwrap_or(512), rotational, wwn, interface_type: Self::get_interface_type(&name, &device_path), smart_status: Self::get_smart_status(&name), }; // Enhance with additional sysfs info if available if device_path.exists() { if drive.model.is_empty() { drive.model = Self::read_sysfs_string(&device_path.join("device/model")); } if drive.serial.is_empty() { drive.serial = Self::read_sysfs_string(&device_path.join("device/serial")); } } drives.push(drive); } } drives } fn gather_storage_controller() -> StorageController { let mut controller = StorageController { name: "Unknown".to_string(), driver: "Unknown".to_string(), }; // Use lspci with JSON output if available if let Ok(output) = Command::new("lspci") .args(["-nn", "-d", "::0100", "-J"]) // Storage controllers class with JSON .output() && output.status.success() && let Ok(json) = serde_json::from_slice::(&output.stdout) && let Some(devices) = json.as_array() { for device in devices { if let Some(device_info) = device.as_object() && let Some(name) = device_info .get("device") .and_then(|v| v.as_object()) .and_then(|v| v.get("name")) .and_then(|v| v.as_str()) { controller.name = name.to_string(); break; } } } // Fallback to text output if JSON fails if controller.name == "Unknown" && let Ok(output) = Command::new("lspci") .args(["-nn", "-d", "::0100"]) // Storage controllers class .output() && output.status.success() { let output_str = String::from_utf8_lossy(&output.stdout); if let Some(line) = output_str.lines().next() { let parts: Vec<&str> = line.split(':').collect(); if parts.len() > 2 { controller.name = parts[2].trim().to_string(); } } } // Try to get driver info from lsmod if let Ok(output) = Command::new("lsmod").output() && output.status.success() { let output_str = String::from_utf8_lossy(&output.stdout); for line in output_str.lines() { if line.contains("ahci") || line.contains("nvme") || line.contains("megaraid") || line.contains("mpt3sas") { let parts: Vec<&str> = line.split_whitespace().collect(); if !parts.is_empty() { controller.driver = parts[0].to_string(); break; } } } } controller } fn gather_memory_modules() -> Vec { let mut modules = Vec::new(); if let Ok(output) = Command::new("dmidecode").arg("--type").arg("17").output() && output.status.success() { let output_str = String::from_utf8_lossy(&output.stdout); let sections: Vec<&str> = output_str.split("Memory Device").collect(); for section in sections.into_iter().skip(1) { let mut module = MemoryModule { size_bytes: 0, speed_mhz: None, manufacturer: None, part_number: None, serial_number: None, rank: None, }; for line in section.lines() { let line = line.trim(); if let Some(size_str) = line.strip_prefix("Size: ") { if size_str != "No Module Installed" && let Some((num, unit)) = size_str.split_once(' ') && let Ok(num) = num.parse::() { module.size_bytes = match unit { "MB" => num * 1024 * 1024, "GB" => num * 1024 * 1024 * 1024, "KB" => num * 1024, _ => 0, }; } } else if let Some(speed_str) = line.strip_prefix("Speed: ") { if let Some((num, _unit)) = speed_str.split_once(' ') { module.speed_mhz = num.parse().ok(); } } else if let Some(man) = line.strip_prefix("Manufacturer: ") { module.manufacturer = Some(man.to_string()); } else if let Some(part) = line.strip_prefix("Part Number: ") { module.part_number = Some(part.to_string()); } else if let Some(serial) = line.strip_prefix("Serial Number: ") { module.serial_number = Some(serial.to_string()); } else if let Some(rank) = line.strip_prefix("Rank: ") { module.rank = rank.parse().ok(); } } if module.size_bytes > 0 { modules.push(module); } } } modules } fn gather_cpus(sys: &System) -> Vec { let mut cpus = Vec::new(); let global_cpu = sys.global_cpu_info(); cpus.push(CPU { model: global_cpu.brand().to_string(), vendor: global_cpu.vendor_id().to_string(), cores: sys.physical_core_count().unwrap_or(1) as u32, threads: sys.cpus().len() as u32, frequency_mhz: global_cpu.frequency(), }); cpus } fn gather_chipset() -> Chipset { Chipset { name: Self::read_dmi("board-product-name").unwrap_or_else(|| "Unknown".to_string()), vendor: Self::read_dmi("board-manufacturer").unwrap_or_else(|| "Unknown".to_string()), } } fn gather_network_interfaces() -> Vec { let mut interfaces = Vec::new(); let sys_net_path = Path::new("/sys/class/net"); if let Ok(entries) = fs::read_dir(sys_net_path) { for entry in entries.flatten() { let iface_name = entry.file_name().into_string().unwrap_or_default(); let iface_path = entry.path(); // Skip virtual interfaces if iface_name.starts_with("lo") || iface_name.starts_with("docker") || iface_name.starts_with("virbr") || iface_name.starts_with("veth") || iface_name.starts_with("br-") || iface_name.starts_with("tun") || iface_name.starts_with("wg") { continue; } // Check if it's a physical interface by looking for device directory if !iface_path.join("device").exists() { continue; } let mac_address = Self::read_sysfs_string(&iface_path.join("address")); let speed_mbps = Self::read_sysfs_u32(&iface_path.join("speed")); let operstate = Self::read_sysfs_string(&iface_path.join("operstate")); let mtu = Self::read_sysfs_u32(&iface_path.join("mtu")).unwrap_or(1500); let driver = Self::read_sysfs_string(&iface_path.join("device/driver/module")); let firmware_version = Self::read_sysfs_opt_string(&iface_path.join("device/firmware_version")); // Get IP addresses using ip command with JSON output let (ipv4_addresses, ipv6_addresses) = Self::get_interface_ips_json(&iface_name); interfaces.push(NetworkInterface { name: iface_name, mac_address, speed_mbps, is_up: operstate == "up", mtu, ipv4_addresses, ipv6_addresses, driver, firmware_version, }); } } interfaces } fn gather_management_interface() -> Option { // Try to detect common management interfaces if Path::new("/dev/ipmi0").exists() { Some(ManagementInterface { kind: "IPMI".to_string(), address: None, firmware: Self::read_dmi("bios-version"), }) } else if Path::new("/sys/class/misc/mei").exists() { Some(ManagementInterface { kind: "Intel ME".to_string(), address: None, firmware: None, }) } else { None } } fn get_host_uuid() -> String { Self::read_dmi("system-uuid").unwrap_or_else(|| uuid::Uuid::new_v4().to_string()) } // Helper methods fn read_sysfs_string(path: &Path) -> String { fs::read_to_string(path) .unwrap_or_default() .trim() .to_string() } fn read_sysfs_opt_string(path: &Path) -> Option { fs::read_to_string(path) .ok() .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) } fn read_sysfs_u32(path: &Path) -> Option { fs::read_to_string(path) .ok() .and_then(|s| s.trim().parse().ok()) } fn read_dmi(field: &str) -> Option { Command::new("dmidecode") .arg("-s") .arg(field) .output() .ok() .filter(|output| output.status.success()) .and_then(|output| String::from_utf8(output.stdout).ok()) .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) } fn get_interface_type(device_name: &str, device_path: &Path) -> String { if device_name.starts_with("nvme") { "NVMe".to_string() } else if device_name.starts_with("sd") { "SATA".to_string() } else if device_name.starts_with("hd") { "IDE".to_string() } else if device_name.starts_with("vd") { "VirtIO".to_string() } else { // Try to determine from device path Self::read_sysfs_string(&device_path.join("device/subsystem")) .split('/') .next_back() .unwrap_or("Unknown") .to_string() } } fn get_smart_status(device_name: &str) -> Option { Command::new("smartctl") .arg("-H") .arg(format!("/dev/{}", device_name)) .output() .ok() .filter(|output| output.status.success()) .and_then(|output| String::from_utf8(output.stdout).ok()) .and_then(|s| { s.lines() .find(|line| line.contains("SMART overall-health self-assessment")) .and_then(|line| line.split(':').nth(1)) .map(|s| s.trim().to_string()) }) } fn parse_size(size_str: &str) -> Option { if size_str.ends_with('T') { size_str[..size_str.len() - 1] .parse::() .ok() .map(|t| t * 1024 * 1024 * 1024 * 1024) } else if size_str.ends_with('G') { size_str[..size_str.len() - 1] .parse::() .ok() .map(|g| g * 1024 * 1024 * 1024) } else if size_str.ends_with('M') { size_str[..size_str.len() - 1] .parse::() .ok() .map(|m| m * 1024 * 1024) } else if size_str.ends_with('K') { size_str[..size_str.len() - 1] .parse::() .ok() .map(|k| k * 1024) } else if size_str.ends_with('B') { size_str[..size_str.len() - 1].parse::().ok() } else { size_str.parse::().ok() } } fn get_interface_ips_json(iface_name: &str) -> (Vec, Vec) { let mut ipv4 = Vec::new(); let mut ipv6 = Vec::new(); // Get IPv4 addresses using JSON output if let Ok(output) = Command::new("ip") .args(["-j", "-4", "addr", "show", iface_name]) .output() && output.status.success() && let Ok(json) = serde_json::from_slice::(&output.stdout) && let Some(addrs) = json.as_array() { for addr_info in addrs { if let Some(addr_info_obj) = addr_info.as_object() && let Some(addr_info) = addr_info_obj.get("addr_info").and_then(|v| v.as_array()) { for addr in addr_info { if let Some(addr_obj) = addr.as_object() && let Some(ip) = addr_obj.get("local").and_then(|v| v.as_str()) { ipv4.push(ip.to_string()); } } } } } // Get IPv6 addresses using JSON output if let Ok(output) = Command::new("ip") .args(["-j", "-6", "addr", "show", iface_name]) .output() && output.status.success() && let Ok(json) = serde_json::from_slice::(&output.stdout) && let Some(addrs) = json.as_array() { for addr_info in addrs { if let Some(addr_info_obj) = addr_info.as_object() && let Some(addr_info) = addr_info_obj.get("addr_info").and_then(|v| v.as_array()) { for addr in addr_info { if let Some(addr_obj) = addr.as_object() && let Some(ip) = addr_obj.get("local").and_then(|v| v.as_str()) { // Skip link-local addresses if !ip.starts_with("fe80::") { ipv6.push(ip.to_string()); } } } } } } (ipv4, ipv6) } }