wip(inventory_agent): Refactoring for better error handling in progress

This commit is contained in:
Jean-Gabriel Gill-Couture 2025-08-19 17:05:23 -04:00
parent 3f34f868eb
commit 6685b05cc5
3 changed files with 143 additions and 29 deletions

4
Cargo.lock generated
View File

@ -105,7 +105,7 @@ dependencies = [
"futures-core", "futures-core",
"futures-util", "futures-util",
"mio 1.0.4", "mio 1.0.4",
"socket2", "socket2 0.5.10",
"tokio", "tokio",
"tracing", "tracing",
] ]
@ -167,7 +167,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"smallvec", "smallvec",
"socket2", "socket2 0.5.10",
"time", "time",
"tracing", "tracing",
"url", "url",

View File

@ -83,20 +83,95 @@ pub struct ManagementInterface {
} }
impl PhysicalHost { impl PhysicalHost {
pub fn gather() -> Self { pub fn gather() -> Result<Self, String> {
let mut sys = System::new_all(); let mut sys = System::new_all();
sys.refresh_all(); sys.refresh_all();
Self { Self::all_tools_available()?;
storage_drives: Self::gather_storage_drives(),
storage_controller: Self::gather_storage_controller(), Ok(Self {
memory_modules: Self::gather_memory_modules(), storage_drives: Self::gather_storage_drives()?,
cpus: Self::gather_cpus(&sys), storage_controller: Self::gather_storage_controller()?,
chipset: Self::gather_chipset(), memory_modules: Self::gather_memory_modules()?,
network_interfaces: Self::gather_network_interfaces(), cpus: Self::gather_cpus(&sys)?,
management_interface: Self::gather_management_interface(), chipset: Self::gather_chipset()?,
host_uuid: Self::get_host_uuid(), network_interfaces: Self::gather_network_interfaces()?,
management_interface: Self::gather_management_interface()?,
host_uuid: Self::get_host_uuid()?,
})
}
fn all_tools_available() -> Result<(), String>{
let required_tools = [
("lsblk", "--version"),
("lspci", "--version"),
("lsmod", "--version"),
("dmidecode", "--version"),
("smartctl", "--version"),
("ip", "route"), // No version flag available
];
let mut missing_tools = Vec::new();
for (tool, tool_arg) in required_tools.iter() {
// First check if tool exists in PATH using which(1)
let exists = if let Ok(output) = Command::new("which").arg(tool).output() {
output.status.success()
} else {
// Fallback: manual PATH search if which(1) is unavailable
if let Ok(path_var) = std::env::var("PATH") {
path_var.split(':').any(|dir| {
let tool_path = std::path::Path::new(dir).join(tool);
tool_path.exists() && Self::is_executable(&tool_path)
})
} else {
false
}
};
if !exists {
missing_tools.push(*tool);
continue;
}
// Verify tool is functional by checking version/help output
let mut cmd = Command::new(tool);
cmd.arg(tool_arg);
cmd.stdout(std::process::Stdio::null());
cmd.stderr(std::process::Stdio::null());
missing_tools.push(*tool);
} }
if !missing_tools.is_empty() {
let missing_str = missing_tools
.iter()
.map(|s| s.to_string())
.collect::<Vec<String>>()
.join(", ");
return Err(format!(
"The following required tools are not available: {}. Please install these tools to use PhysicalHost::gather()",
missing_str
));
}
Ok(())
}
#[cfg(unix)]
fn is_executable(path: &std::path::Path) -> bool {
use std::os::unix::fs::PermissionsExt;
match std::fs::metadata(path) {
Ok(meta) => meta.permissions().mode() & 0o111 != 0,
Err(_) => false,
}
}
#[cfg(not(unix))]
fn is_executable(_path: &std::path::Path) -> bool {
// On non-Unix systems, we assume existence implies executability
true
} }
fn gather_storage_drives() -> Vec<StorageDrive> { fn gather_storage_drives() -> Vec<StorageDrive> {
@ -331,11 +406,11 @@ impl PhysicalHost {
cpus cpus
} }
fn gather_chipset() -> Chipset { fn gather_chipset() -> Result<Chipset, String> {
Chipset { Ok(Chipset {
name: Self::read_dmi("board-product-name").unwrap_or_else(|| "Unknown".to_string()), name: Self::read_dmi("board-product-name")?,
vendor: Self::read_dmi("board-manufacturer").unwrap_or_else(|| "Unknown".to_string()), vendor: Self::read_dmi("board-manufacturer")?,
} })
} }
fn gather_network_interfaces() -> Vec<NetworkInterface> { fn gather_network_interfaces() -> Vec<NetworkInterface> {
@ -436,16 +511,47 @@ impl PhysicalHost {
.and_then(|s| s.trim().parse().ok()) .and_then(|s| s.trim().parse().ok())
} }
fn read_dmi(field: &str) -> Option<String> { // Valid string keywords are:
Command::new("dmidecode") // bios-vendor
.arg("-s") // bios-version
.arg(field) // bios-release-date
.output() // bios-revision
.ok() // firmware-revision
.filter(|output| output.status.success()) // system-manufacturer
.and_then(|output| String::from_utf8(output.stdout).ok()) // system-product-name
.map(|s| s.trim().to_string()) // system-version
.filter(|s| !s.is_empty()) // 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}")),
}
} }
fn get_interface_type(device_name: &str, device_path: &Path) -> String { fn get_interface_type(device_name: &str, device_path: &Path) -> String {

View File

@ -9,8 +9,16 @@ mod hwinfo;
async fn inventory() -> impl Responder { async fn inventory() -> impl Responder {
log::info!("Received inventory request"); log::info!("Received inventory request");
let host = PhysicalHost::gather(); let host = PhysicalHost::gather();
log::info!("Inventory data gathered successfully"); match host {
actix_web::HttpResponse::Ok().json(host) Ok(host) => {
log::info!("Inventory data gathered successfully");
actix_web::HttpResponse::Ok().json(host)
}
Err(error) => {
log::error!("Inventory data gathering FAILED");
actix_web::HttpResponse::InternalServerError().json(error)
}
}
} }
#[actix_web::main] #[actix_web::main]