fix(inventory_agent) : Agent now retreives correct dmidecode fields, fixed uuid generation which is unacceptable, fixed storage drive parsing, much better error handling, much more strict behavior which also leads to more complete output as missing fields will raise errors unless explicitely optional

This commit is contained in:
Jean-Gabriel Gill-Couture 2025-08-19 17:56:06 -04:00
parent 6685b05cc5
commit 72fb05b5cc
3 changed files with 477 additions and 331 deletions

1
Cargo.lock generated
View File

@ -2178,7 +2178,6 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sysinfo", "sysinfo",
"uuid",
] ]
[[package]] [[package]]

View File

@ -10,4 +10,3 @@ serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
log.workspace = true log.workspace = true
env_logger.workspace = true env_logger.workspace = true
uuid.workspace = true

View File

@ -1,3 +1,4 @@
use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::fs; use std::fs;
@ -101,7 +102,7 @@ impl PhysicalHost {
}) })
} }
fn all_tools_available() -> Result<(), String>{ fn all_tools_available() -> Result<(), String> {
let required_tools = [ let required_tools = [
("lsblk", "--version"), ("lsblk", "--version"),
("lspci", "--version"), ("lspci", "--version"),
@ -140,7 +141,13 @@ impl PhysicalHost {
cmd.stdout(std::process::Stdio::null()); cmd.stdout(std::process::Stdio::null());
cmd.stderr(std::process::Stdio::null()); cmd.stderr(std::process::Stdio::null());
missing_tools.push(*tool); if let Ok(status) = cmd.status() {
if !status.success() {
missing_tools.push(*tool);
}
} else {
missing_tools.push(*tool);
}
} }
if !missing_tools.is_empty() { if !missing_tools.is_empty() {
@ -174,11 +181,11 @@ impl PhysicalHost {
true true
} }
fn gather_storage_drives() -> Vec<StorageDrive> { fn gather_storage_drives() -> Result<Vec<StorageDrive>, String> {
let mut drives = Vec::new(); let mut drives = Vec::new();
// Use lsblk with JSON output for robust parsing // Use lsblk with JSON output for robust parsing
if let Ok(output) = Command::new("lsblk") let output = Command::new("lsblk")
.args([ .args([
"-d", "-d",
"-o", "-o",
@ -189,132 +196,165 @@ impl PhysicalHost {
"--json", "--json",
]) ])
.output() .output()
&& output.status.success() .map_err(|e| format!("Failed to execute lsblk: {}", e))?;
&& let Ok(json) = serde_json::from_slice::<Value>(&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 if !output.status.success() {
.get("model") return Err(format!(
.and_then(|v| v.as_str()) "lsblk command failed: {}",
.map(|s| s.trim().to_string()) String::from_utf8_lossy(&output.stderr)
.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 let json: Value = serde_json::from_slice(&output.stdout)
.map_err(|e| format!("Failed to parse lsblk JSON output: {}", e))?;
let blockdevices = json
.get("blockdevices")
.and_then(|v| v.as_array())
.ok_or("Invalid lsblk JSON: missing 'blockdevices' array")?;
for device in blockdevices {
let name = device
.get("name")
.and_then(|v| v.as_str())
.ok_or("Missing 'name' in lsblk device")?
.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())
.ok_or("Missing 'size' in lsblk device")?;
let size_bytes = Self::parse_size(size_str)?;
let rotational = device
.get("rota")
.and_then(|v| v.as_bool())
.ok_or("Missing 'rota' in lsblk device")?;
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 logical_block_size = Self::read_sysfs_u32(
&device_path.join("queue/logical_block_size"),
)
.map_err(|e| format!("Failed to read logical block size for {}: {}", name, e))?;
let physical_block_size = Self::read_sysfs_u32(
&device_path.join("queue/physical_block_size"),
)
.map_err(|e| format!("Failed to read physical block size for {}: {}", name, e))?;
let interface_type = Self::get_interface_type(&name, &device_path)?;
let smart_status = Self::get_smart_status(&name)?;
let mut drive = StorageDrive {
name: name.clone(),
model,
serial,
size_bytes,
logical_block_size,
physical_block_size,
rotational,
wwn,
interface_type,
smart_status,
};
// 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"))
.map_err(|e| format!("Failed to read model for {}: {}", name, e))?;
}
if drive.serial.is_empty() {
drive.serial = Self::read_sysfs_string(&device_path.join("device/serial"))
.map_err(|e| format!("Failed to read serial for {}: {}", name, e))?;
}
}
drives.push(drive);
}
Ok(drives)
} }
fn gather_storage_controller() -> StorageController { fn gather_storage_controller() -> Result<StorageController, String> {
let mut controller = StorageController { let mut controller = StorageController {
name: "Unknown".to_string(), name: "Unknown".to_string(),
driver: "Unknown".to_string(), driver: "Unknown".to_string(),
}; };
// Use lspci with JSON output if available // Use lspci with JSON output if available
if let Ok(output) = Command::new("lspci") let output = Command::new("lspci")
.args(["-nn", "-d", "::0100", "-J"]) // Storage controllers class with JSON .args(["-nn", "-d", "::0100", "-J"]) // Storage controllers class with JSON
.output() .output()
&& output.status.success() .map_err(|e| format!("Failed to execute lspci: {}", e))?;
&& let Ok(json) = serde_json::from_slice::<Value>(&output.stdout)
&& let Some(devices) = json.as_array() if output.status.success() {
{ let json: Value = serde_json::from_slice(&output.stdout)
for device in devices { .map_err(|e| format!("Failed to parse lspci JSON output: {}", e))?;
if let Some(device_info) = device.as_object()
&& let Some(name) = device_info if let Some(devices) = json.as_array() {
.get("device") for device in devices {
.and_then(|v| v.as_object()) if let Some(device_info) = device.as_object()
.and_then(|v| v.get("name")) && let Some(name) = device_info
.and_then(|v| v.as_str()) .get("device")
{ .and_then(|v| v.as_object())
controller.name = name.to_string(); .and_then(|v| v.get("name"))
break; .and_then(|v| v.as_str())
{
controller.name = name.to_string();
break;
}
} }
} }
} }
// Fallback to text output if JSON fails // Fallback to text output if JSON fails or no device found
if controller.name == "Unknown" if controller.name == "Unknown" {
&& let Ok(output) = Command::new("lspci") let output = Command::new("lspci")
.args(["-nn", "-d", "::0100"]) // Storage controllers class .args(["-nn", "-d", "::0100"]) // Storage controllers class
.output() .output()
&& output.status.success() .map_err(|e| format!("Failed to execute lspci (fallback): {}", e))?;
{
let output_str = String::from_utf8_lossy(&output.stdout); if output.status.success() {
if let Some(line) = output_str.lines().next() { let output_str = String::from_utf8_lossy(&output.stdout);
let parts: Vec<&str> = line.split(':').collect(); if let Some(line) = output_str.lines().next() {
if parts.len() > 2 { let parts: Vec<&str> = line.split(':').collect();
controller.name = parts[2].trim().to_string(); if parts.len() > 2 {
controller.name = parts[2].trim().to_string();
}
} }
} }
} }
// Try to get driver info from lsmod // Try to get driver info from lsmod
if let Ok(output) = Command::new("lsmod").output() let output = Command::new("lsmod")
&& output.status.success() .output()
{ .map_err(|e| format!("Failed to execute lsmod: {}", e))?;
if output.status.success() {
let output_str = String::from_utf8_lossy(&output.stdout); let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines() { for line in output_str.lines() {
if line.contains("ahci") if line.contains("ahci")
@ -331,67 +371,78 @@ impl PhysicalHost {
} }
} }
controller Ok(controller)
} }
fn gather_memory_modules() -> Vec<MemoryModule> { fn gather_memory_modules() -> Result<Vec<MemoryModule>, String> {
let mut modules = Vec::new(); let mut modules = Vec::new();
if let Ok(output) = Command::new("dmidecode").arg("--type").arg("17").output() let output = Command::new("dmidecode")
&& output.status.success() .arg("--type")
{ .arg("17")
let output_str = String::from_utf8_lossy(&output.stdout); .output()
let sections: Vec<&str> = output_str.split("Memory Device").collect(); .map_err(|e| format!("Failed to execute dmidecode: {}", e))?;
for section in sections.into_iter().skip(1) { if !output.status.success() {
let mut module = MemoryModule { return Err(format!(
size_bytes: 0, "dmidecode command failed: {}",
speed_mhz: None, String::from_utf8_lossy(&output.stderr)
manufacturer: None, ));
part_number: None, }
serial_number: None,
rank: None,
};
for line in section.lines() { let output_str = String::from_utf8(output.stdout)
let line = line.trim(); .map_err(|e| format!("Failed to parse dmidecode output: {}", e))?;
if let Some(size_str) = line.strip_prefix("Size: ") {
if size_str != "No Module Installed" let sections: Vec<&str> = output_str.split("Memory Device").collect();
&& let Some((num, unit)) = size_str.split_once(' ')
&& let Ok(num) = num.parse::<u64>() for section in sections.into_iter().skip(1) {
{ let mut module = MemoryModule {
module.size_bytes = match unit { size_bytes: 0,
"MB" => num * 1024 * 1024, speed_mhz: None,
"GB" => num * 1024 * 1024 * 1024, manufacturer: None,
"KB" => num * 1024, part_number: None,
_ => 0, serial_number: None,
}; rank: None,
} };
} else if let Some(speed_str) = line.strip_prefix("Speed: ") {
if let Some((num, _unit)) = speed_str.split_once(' ') { for line in section.lines() {
module.speed_mhz = num.parse().ok(); let line = line.trim();
} if let Some(size_str) = line.strip_prefix("Size: ") {
} else if let Some(man) = line.strip_prefix("Manufacturer: ") { if size_str != "No Module Installed"
module.manufacturer = Some(man.to_string()); && let Some((num, unit)) = size_str.split_once(' ')
} else if let Some(part) = line.strip_prefix("Part Number: ") { && let Ok(num) = num.parse::<u64>()
module.part_number = Some(part.to_string()); {
} else if let Some(serial) = line.strip_prefix("Serial Number: ") { module.size_bytes = match unit {
module.serial_number = Some(serial.to_string()); "MB" => num * 1024 * 1024,
} else if let Some(rank) = line.strip_prefix("Rank: ") { "GB" => num * 1024 * 1024 * 1024,
module.rank = rank.parse().ok(); "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 { if module.size_bytes > 0 {
modules.push(module); modules.push(module);
}
} }
} }
modules Ok(modules)
} }
fn gather_cpus(sys: &System) -> Vec<CPU> { fn gather_cpus(sys: &System) -> Result<Vec<CPU>, String> {
let mut cpus = Vec::new(); let mut cpus = Vec::new();
let global_cpu = sys.global_cpu_info(); let global_cpu = sys.global_cpu_info();
@ -403,232 +454,310 @@ impl PhysicalHost {
frequency_mhz: global_cpu.frequency(), frequency_mhz: global_cpu.frequency(),
}); });
cpus Ok(cpus)
} }
fn gather_chipset() -> Result<Chipset, String> { fn gather_chipset() -> Result<Chipset, String> {
Ok(Chipset { Ok(Chipset {
name: Self::read_dmi("board-product-name")?, name: Self::read_dmi("baseboard-product-name")?,
vendor: Self::read_dmi("board-manufacturer")?, vendor: Self::read_dmi("baseboard-manufacturer")?,
}) })
} }
fn gather_network_interfaces() -> Vec<NetworkInterface> { fn gather_network_interfaces() -> Result<Vec<NetworkInterface>, String> {
let mut interfaces = Vec::new(); let mut interfaces = Vec::new();
let sys_net_path = Path::new("/sys/class/net"); let sys_net_path = Path::new("/sys/class/net");
if let Ok(entries) = fs::read_dir(sys_net_path) { let entries = fs::read_dir(sys_net_path)
for entry in entries.flatten() { .map_err(|e| format!("Failed to read /sys/class/net: {}", e))?;
let iface_name = entry.file_name().into_string().unwrap_or_default();
let iface_path = entry.path();
// Skip virtual interfaces for entry in entries {
if iface_name.starts_with("lo") let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
|| iface_name.starts_with("docker") let iface_name = entry
|| iface_name.starts_with("virbr") .file_name()
|| iface_name.starts_with("veth") .into_string()
|| iface_name.starts_with("br-") .map_err(|_| "Invalid UTF-8 in interface name")?;
|| iface_name.starts_with("tun") let iface_path = entry.path();
|| iface_name.starts_with("wg")
{
continue;
}
// Check if it's a physical interface by looking for device directory // Skip virtual interfaces
if !iface_path.join("device").exists() { if iface_name.starts_with("lo")
continue; || iface_name.starts_with("docker")
} || iface_name.starts_with("virbr")
|| iface_name.starts_with("veth")
let mac_address = Self::read_sysfs_string(&iface_path.join("address")); || iface_name.starts_with("br-")
let speed_mbps = Self::read_sysfs_u32(&iface_path.join("speed")); || iface_name.starts_with("tun")
let operstate = Self::read_sysfs_string(&iface_path.join("operstate")); || iface_name.starts_with("wg")
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")); continue;
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,
});
} }
// 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"))
.map_err(|e| format!("Failed to read MAC address for {}: {}", iface_name, e))?;
let speed_mbps = if iface_path.join("speed").exists() {
match Self::read_sysfs_u32(&iface_path.join("speed")) {
Ok(speed) => Some(speed),
Err(e) => {
debug!(
"Failed to read speed for {}: {} . This is expected to fail on wifi interfaces.",
iface_name, e
);
None
}
}
} else {
None
};
let operstate = Self::read_sysfs_string(&iface_path.join("operstate"))
.map_err(|e| format!("Failed to read operstate for {}: {}", iface_name, e))?;
let mtu = Self::read_sysfs_u32(&iface_path.join("mtu"))
.map_err(|e| format!("Failed to read MTU for {}: {}", iface_name, e))?;
let driver =
Self::read_sysfs_symlink_basename(&iface_path.join("device/driver/module"))
.map_err(|e| format!("Failed to read driver for {}: {}", iface_name, e))?;
let firmware_version = Self::read_sysfs_opt_string(
&iface_path.join("device/firmware_version"),
)
.map_err(|e| format!("Failed to read firmware version for {}: {}", iface_name, e))?;
// Get IP addresses using ip command with JSON output
let (ipv4_addresses, ipv6_addresses) = Self::get_interface_ips_json(&iface_name)
.map_err(|e| format!("Failed to get IP addresses for {}: {}", iface_name, e))?;
interfaces.push(NetworkInterface {
name: iface_name,
mac_address,
speed_mbps,
is_up: operstate == "up",
mtu,
ipv4_addresses,
ipv6_addresses,
driver,
firmware_version,
});
} }
interfaces Ok(interfaces)
} }
fn gather_management_interface() -> Option<ManagementInterface> { fn gather_management_interface() -> Result<Option<ManagementInterface>, String> {
// Try to detect common management interfaces
if Path::new("/dev/ipmi0").exists() { if Path::new("/dev/ipmi0").exists() {
Some(ManagementInterface { Ok(Some(ManagementInterface {
kind: "IPMI".to_string(), kind: "IPMI".to_string(),
address: None, address: None,
firmware: Self::read_dmi("bios-version"), firmware: Some(Self::read_dmi("bios-version")?),
}) }))
} else if Path::new("/sys/class/misc/mei").exists() { } else if Path::new("/sys/class/misc/mei").exists() {
Some(ManagementInterface { Ok(Some(ManagementInterface {
kind: "Intel ME".to_string(), kind: "Intel ME".to_string(),
address: None, address: None,
firmware: None, firmware: None,
}) }))
} else { } else {
None Ok(None)
} }
} }
fn get_host_uuid() -> String { fn get_host_uuid() -> Result<String, String> {
Self::read_dmi("system-uuid").unwrap() Self::read_dmi("system-uuid")
} }
// Helper methods // Helper methods
fn read_sysfs_string(path: &Path) -> String { fn read_sysfs_string(path: &Path) -> Result<String, String> {
fs::read_to_string(path) fs::read_to_string(path)
.unwrap_or_default()
.trim()
.to_string()
}
fn read_sysfs_opt_string(path: &Path) -> Option<String> {
fs::read_to_string(path)
.ok()
.map(|s| s.trim().to_string()) .map(|s| s.trim().to_string())
.filter(|s| !s.is_empty()) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))
} }
fn read_sysfs_u32(path: &Path) -> Option<u32> { fn read_sysfs_opt_string(path: &Path) -> Result<Option<String>, String> {
fs::read_to_string(path) match fs::read_to_string(path) {
.ok() Ok(s) => {
.and_then(|s| s.trim().parse().ok()) let s = s.trim().to_string();
} Ok(if s.is_empty() { None } else { Some(s) })
// Valid string keywords are:
// bios-vendor
// bios-version
// bios-release-date
// bios-revision
// firmware-revision
// system-manufacturer
// system-product-name
// system-version
// system-serial-number
// system-uuid
// system-sku-number
// system-family
// baseboard-manufacturer
// baseboard-product-name
// baseboard-version
// baseboard-serial-number
// baseboard-asset-tag
// chassis-manufacturer
// chassis-type
// chassis-version
// chassis-serial-number
// chassis-asset-tag
// processor-family
// processor-manufacturer
// processor-version
// processor-frequency
fn read_dmi(field: &str) -> Result<String, String> {
match Command::new("dmidecode").arg("-s").arg(field).output() {
Ok(output) => {
let stdout = String::from_utf8(output.stdout).expect("Output should parse as utf8");
if output.status.success() && stdout.is_empty() {
return Ok(stdout);
} else {
return Err(format!(
"dmidecode command failed for field {field} : {stdout}"
));
}
} }
Err(e) => Err(format!("dmidecode command failed for field {field} : {e}")), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(format!("Failed to read {}: {}", path.display(), e)),
} }
} }
fn get_interface_type(device_name: &str, device_path: &Path) -> String { fn read_sysfs_u32(path: &Path) -> Result<u32, String> {
if device_name.starts_with("nvme") { fs::read_to_string(path)
"NVMe".to_string() .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?
} else if device_name.starts_with("sd") { .trim()
"SATA".to_string() .parse()
} else if device_name.starts_with("hd") { .map_err(|e| format!("Failed to parse {}: {}", path.display(), e))
"IDE".to_string() }
} else if device_name.starts_with("vd") {
"VirtIO".to_string() fn read_sysfs_symlink_basename(path: &Path) -> Result<String, String> {
} else { match fs::read_link(path) {
// Try to determine from device path Ok(target_path) => match target_path.file_name() {
Self::read_sysfs_string(&device_path.join("device/subsystem")) Some(name_osstr) => match name_osstr.to_str() {
.split('/') Some(name_str) => Ok(name_str.to_string()),
.next_back() None => Err(format!(
.unwrap_or("Unknown") "Symlink target basename is not valid UTF-8: {}",
.to_string() target_path.display()
)),
},
None => Err(format!(
"Symlink target has no basename: {} -> {}",
path.display(),
target_path.display()
)),
},
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(format!(
"Could not resolve symlink for path : {}",
path.display()
)),
Err(e) => Err(format!("Failed to read symlink {}: {}", path.display(), e)),
} }
} }
fn get_smart_status(device_name: &str) -> Option<String> { fn read_dmi(field: &str) -> Result<String, String> {
Command::new("smartctl") let output = Command::new("dmidecode")
.arg("-H") .arg("-s")
.arg(format!("/dev/{}", device_name)) .arg(field)
.output() .output()
.ok() .map_err(|e| format!("Failed to execute dmidecode for field {}: {}", field, e))?;
.filter(|output| output.status.success())
.and_then(|output| String::from_utf8(output.stdout).ok()) if !output.status.success() {
.and_then(|s| { return Err(format!(
s.lines() "dmidecode command failed for field {}: {}",
.find(|line| line.contains("SMART overall-health self-assessment")) field,
.and_then(|line| line.split(':').nth(1)) String::from_utf8_lossy(&output.stderr)
.map(|s| s.trim().to_string()) ));
}
String::from_utf8(output.stdout)
.map(|s| s.trim().to_string())
.map_err(|e| {
format!(
"Failed to parse dmidecode output for field {}: {}",
field, e
)
}) })
} }
fn parse_size(size_str: &str) -> Option<u64> { fn get_interface_type(device_name: &str, device_path: &Path) -> Result<String, String> {
if size_str.ends_with('T') { if device_name.starts_with("nvme") {
size_str[..size_str.len() - 1] Ok("NVMe".to_string())
.parse::<u64>() } else if device_name.starts_with("sd") {
.ok() Ok("SATA".to_string())
.map(|t| t * 1024 * 1024 * 1024 * 1024) } else if device_name.starts_with("hd") {
} else if size_str.ends_with('G') { Ok("IDE".to_string())
size_str[..size_str.len() - 1] } else if device_name.starts_with("vd") {
.parse::<u64>() Ok("VirtIO".to_string())
.ok()
.map(|g| g * 1024 * 1024 * 1024)
} else if size_str.ends_with('M') {
size_str[..size_str.len() - 1]
.parse::<u64>()
.ok()
.map(|m| m * 1024 * 1024)
} else if size_str.ends_with('K') {
size_str[..size_str.len() - 1]
.parse::<u64>()
.ok()
.map(|k| k * 1024)
} else if size_str.ends_with('B') {
size_str[..size_str.len() - 1].parse::<u64>().ok()
} else { } else {
size_str.parse::<u64>().ok() // Try to determine from device path
let subsystem = Self::read_sysfs_string(&device_path.join("device/subsystem"))?;
Ok(subsystem
.split('/')
.next_back()
.unwrap_or("Unknown")
.to_string())
} }
} }
fn get_interface_ips_json(iface_name: &str) -> (Vec<String>, Vec<String>) { fn get_smart_status(device_name: &str) -> Result<Option<String>, String> {
let output = Command::new("smartctl")
.arg("-H")
.arg(format!("/dev/{}", device_name))
.output()
.map_err(|e| format!("Failed to execute smartctl for {}: {}", device_name, e))?;
if !output.status.success() {
return Ok(None);
}
let stdout = String::from_utf8(output.stdout)
.map_err(|e| format!("Failed to parse smartctl output for {}: {}", device_name, e))?;
for line in stdout.lines() {
if line.contains("SMART overall-health self-assessment") {
if let Some(status) = line.split(':').nth(1) {
return Ok(Some(status.trim().to_string()));
}
}
}
Ok(None)
}
fn parse_size(size_str: &str) -> Result<u64, String> {
debug!("Parsing size_str '{size_str}'");
let size;
if size_str.ends_with('T') {
size = size_str[..size_str.len() - 1]
.parse::<f64>()
.map(|t| t * 1024.0 * 1024.0 * 1024.0 * 1024.0)
.map_err(|e| format!("Failed to parse T size '{}': {}", size_str, e))
} else if size_str.ends_with('G') {
size = size_str[..size_str.len() - 1]
.parse::<f64>()
.map(|g| g * 1024.0 * 1024.0 * 1024.0)
.map_err(|e| format!("Failed to parse G size '{}': {}", size_str, e))
} else if size_str.ends_with('M') {
size = size_str[..size_str.len() - 1]
.parse::<f64>()
.map(|m| m * 1024.0 * 1024.0)
.map_err(|e| format!("Failed to parse M size '{}': {}", size_str, e))
} else if size_str.ends_with('K') {
size = size_str[..size_str.len() - 1]
.parse::<f64>()
.map(|k| k * 1024.0)
.map_err(|e| format!("Failed to parse K size '{}': {}", size_str, e))
} else if size_str.ends_with('B') {
size = size_str[..size_str.len() - 1]
.parse::<f64>()
.map_err(|e| format!("Failed to parse B size '{}': {}", size_str, e))
} else {
size = size_str
.parse::<f64>()
.map_err(|e| format!("Failed to parse size '{}': {}", size_str, e))
}
size.map(|s| s as u64)
}
fn get_interface_ips_json(iface_name: &str) -> Result<(Vec<String>, Vec<String>), String> {
let mut ipv4 = Vec::new(); let mut ipv4 = Vec::new();
let mut ipv6 = Vec::new(); let mut ipv6 = Vec::new();
// Get IPv4 addresses using JSON output // Get IPv4 addresses using JSON output
if let Ok(output) = Command::new("ip") let output = Command::new("ip")
.args(["-j", "-4", "addr", "show", iface_name]) .args(["-j", "-4", "addr", "show", iface_name])
.output() .output()
&& output.status.success() .map_err(|e| {
&& let Ok(json) = serde_json::from_slice::<Value>(&output.stdout) format!(
&& let Some(addrs) = json.as_array() "Failed to execute ip command for IPv4 on {}: {}",
{ iface_name, e
)
})?;
if !output.status.success() {
return Err(format!(
"ip command for IPv4 on {} failed: {}",
iface_name,
String::from_utf8_lossy(&output.stderr)
));
}
let json: Value = serde_json::from_slice(&output.stdout).map_err(|e| {
format!(
"Failed to parse ip JSON output for IPv4 on {}: {}",
iface_name, e
)
})?;
if let Some(addrs) = json.as_array() {
for addr_info in addrs { for addr_info in addrs {
if let Some(addr_info_obj) = addr_info.as_object() if let Some(addr_info_obj) = addr_info.as_object()
&& let Some(addr_info) = && let Some(addr_info) =
@ -646,13 +775,32 @@ impl PhysicalHost {
} }
// Get IPv6 addresses using JSON output // Get IPv6 addresses using JSON output
if let Ok(output) = Command::new("ip") let output = Command::new("ip")
.args(["-j", "-6", "addr", "show", iface_name]) .args(["-j", "-6", "addr", "show", iface_name])
.output() .output()
&& output.status.success() .map_err(|e| {
&& let Ok(json) = serde_json::from_slice::<Value>(&output.stdout) format!(
&& let Some(addrs) = json.as_array() "Failed to execute ip command for IPv6 on {}: {}",
{ iface_name, e
)
})?;
if !output.status.success() {
return Err(format!(
"ip command for IPv6 on {} failed: {}",
iface_name,
String::from_utf8_lossy(&output.stderr)
));
}
let json: Value = serde_json::from_slice(&output.stdout).map_err(|e| {
format!(
"Failed to parse ip JSON output for IPv6 on {}: {}",
iface_name, e
)
})?;
if let Some(addrs) = json.as_array() {
for addr_info in addrs { for addr_info in addrs {
if let Some(addr_info_obj) = addr_info.as_object() if let Some(addr_info_obj) = addr_info.as_object()
&& let Some(addr_info) = && let Some(addr_info) =
@ -672,6 +820,6 @@ impl PhysicalHost {
} }
} }
(ipv4, ipv6) Ok((ipv4, ipv6))
} }
} }