1001 lines
35 KiB
Rust
1001 lines
35 KiB
Rust
use harmony_types::net::MacAddress;
|
||
use log::{debug, trace, warn};
|
||
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<StorageDrive>,
|
||
pub storage_controller: StorageController,
|
||
pub memory_modules: Vec<MemoryModule>,
|
||
pub cpus: Vec<CPU>,
|
||
pub chipset: Chipset,
|
||
pub network_interfaces: Vec<NetworkInterface>,
|
||
pub management_interface: Option<ManagementInterface>,
|
||
pub host_uuid: String,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||
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<String>,
|
||
pub interface_type: String,
|
||
pub smart_status: Option<String>,
|
||
}
|
||
|
||
impl StorageDrive {
|
||
pub fn dummy() -> Self {
|
||
Self {
|
||
name: String::new(),
|
||
model: String::new(),
|
||
serial: String::new(),
|
||
size_bytes: 0,
|
||
logical_block_size: 0,
|
||
physical_block_size: 0,
|
||
rotational: false,
|
||
wwn: None,
|
||
interface_type: String::new(),
|
||
smart_status: None,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug)]
|
||
pub struct StorageController {
|
||
pub name: String,
|
||
pub driver: String,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||
pub struct MemoryModule {
|
||
pub size_bytes: u64,
|
||
pub speed_mhz: Option<u32>,
|
||
pub manufacturer: Option<String>,
|
||
pub part_number: Option<String>,
|
||
pub serial_number: Option<String>,
|
||
pub rank: Option<u8>,
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||
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, Clone)]
|
||
pub struct NetworkInterface {
|
||
pub name: String,
|
||
pub mac_address: MacAddress,
|
||
pub speed_mbps: Option<u32>,
|
||
pub is_up: bool,
|
||
pub mtu: u32,
|
||
pub ipv4_addresses: Vec<String>,
|
||
pub ipv6_addresses: Vec<String>,
|
||
pub driver: String,
|
||
pub firmware_version: Option<String>,
|
||
}
|
||
|
||
impl NetworkInterface {
|
||
pub fn dummy() -> Self {
|
||
use harmony_macros::mac_address;
|
||
|
||
Self {
|
||
name: String::new(),
|
||
mac_address: mac_address!("00:00:00:00:00:00"),
|
||
speed_mbps: Some(0),
|
||
is_up: false,
|
||
mtu: 0,
|
||
ipv4_addresses: vec![],
|
||
ipv6_addresses: vec![],
|
||
driver: String::new(),
|
||
firmware_version: None,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Serialize, Deserialize, Debug)]
|
||
pub struct ManagementInterface {
|
||
pub kind: String,
|
||
pub address: Option<String>,
|
||
pub firmware: Option<String>,
|
||
}
|
||
|
||
impl PhysicalHost {
|
||
pub fn gather() -> Result<Self, String> {
|
||
trace!("Start gathering physical host information");
|
||
let mut sys = System::new_all();
|
||
trace!("System new_all called");
|
||
sys.refresh_all();
|
||
trace!("System refresh_all called");
|
||
|
||
Self::all_tools_available()?;
|
||
|
||
trace!("All tools_available success");
|
||
|
||
let storage_drives = Self::gather_storage_drives()?;
|
||
trace!("got storage drives");
|
||
|
||
let storage_controller = Self::gather_storage_controller()?;
|
||
trace!("got storage controller");
|
||
|
||
let memory_modules = Self::gather_memory_modules()?;
|
||
trace!("got memory_modules");
|
||
|
||
let cpus = Self::gather_cpus(&sys)?;
|
||
trace!("got cpus");
|
||
|
||
let chipset = Self::gather_chipset()?;
|
||
trace!("got chipsets");
|
||
|
||
let network_interfaces = Self::gather_network_interfaces()?;
|
||
trace!("got network_interfaces");
|
||
|
||
let management_interface = Self::gather_management_interface()?;
|
||
trace!("got management_interface");
|
||
|
||
let host_uuid = Self::get_host_uuid()?;
|
||
|
||
Ok(Self {
|
||
storage_drives,
|
||
storage_controller,
|
||
memory_modules,
|
||
cpus,
|
||
chipset,
|
||
network_interfaces,
|
||
management_interface,
|
||
host_uuid,
|
||
})
|
||
}
|
||
|
||
fn all_tools_available() -> Result<(), String> {
|
||
let required_tools = [
|
||
("lsblk", Some("--version")),
|
||
("lspci", Some("--version")),
|
||
("lsmod", None),
|
||
("dmidecode", Some("--version")),
|
||
("smartctl", Some("--version")),
|
||
("ip", Some("route")), // No version flag available
|
||
];
|
||
|
||
let mut missing_tools = Vec::new();
|
||
|
||
debug!("Looking for required_tools {required_tools:?}");
|
||
for (tool, tool_arg) in required_tools.iter() {
|
||
// First check if tool exists in PATH using which(1)
|
||
let mut exists = if let Ok(output) = Command::new("which").arg(tool).output() {
|
||
output.status.success()
|
||
} else {
|
||
false
|
||
};
|
||
|
||
if !exists {
|
||
// Fallback: manual PATH search if which(1) is unavailable
|
||
debug!("Looking for {tool} in path");
|
||
if let Ok(path_var) = std::env::var("PATH") {
|
||
debug!("PATH is {path_var}");
|
||
exists = path_var.split(':').any(|dir| {
|
||
let tool_path = std::path::Path::new(dir).join(tool);
|
||
tool_path.exists() && Self::is_executable(&tool_path)
|
||
})
|
||
}
|
||
}
|
||
|
||
if !exists {
|
||
warn!("Unable to find tool {tool} from PATH");
|
||
missing_tools.push(*tool);
|
||
continue;
|
||
}
|
||
|
||
// Verify tool is functional by checking version/help output
|
||
let mut cmd = Command::new(tool);
|
||
if let Some(tool_arg) = tool_arg {
|
||
cmd.arg(tool_arg);
|
||
}
|
||
cmd.stdout(std::process::Stdio::null());
|
||
cmd.stderr(std::process::Stdio::null());
|
||
|
||
if let Ok(status) = cmd.status() {
|
||
if !status.success() {
|
||
warn!("Unable to test {tool} status failed");
|
||
missing_tools.push(*tool);
|
||
}
|
||
} else {
|
||
warn!("Unable to test {tool}");
|
||
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
|
||
));
|
||
}
|
||
|
||
debug!("All tools found!");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(unix)]
|
||
fn is_executable(path: &std::path::Path) -> bool {
|
||
debug!("Checking if {} is executable", path.to_string_lossy());
|
||
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() -> Result<Vec<StorageDrive>, String> {
|
||
let mut drives = Vec::new();
|
||
|
||
trace!("Starting storage drive discovery using lsblk");
|
||
|
||
// Use lsblk with JSON output for robust parsing
|
||
trace!("Executing 'lsblk -d -o NAME,MODEL,SERIAL,SIZE,ROTA,WWN -n -e 7 --json'");
|
||
let output = Command::new("lsblk")
|
||
.args([
|
||
"-d",
|
||
"-o",
|
||
"NAME,MODEL,SERIAL,SIZE,ROTA,WWN",
|
||
"-n",
|
||
"-e",
|
||
"7",
|
||
"--json",
|
||
])
|
||
.output()
|
||
.map_err(|e| format!("Failed to execute lsblk: {}", e))?;
|
||
|
||
trace!(
|
||
"lsblk command executed successfully (status: {:?})",
|
||
output.status
|
||
);
|
||
|
||
if !output.status.success() {
|
||
let stderr_str = String::from_utf8_lossy(&output.stderr);
|
||
debug!("lsblk command failed: {stderr_str}");
|
||
return Err(format!("lsblk command failed: {stderr_str}"));
|
||
}
|
||
|
||
trace!("Parsing lsblk JSON output");
|
||
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")?;
|
||
|
||
trace!("Found {} blockdevices in lsblk output", blockdevices.len());
|
||
|
||
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() {
|
||
trace!("Skipping unnamed device entry: {:?}", device);
|
||
continue;
|
||
}
|
||
|
||
trace!("Inspecting block device: {name}");
|
||
|
||
// Extract metadata fields
|
||
let model = device
|
||
.get("model")
|
||
.and_then(|v| v.as_str())
|
||
.map(|s| s.trim().to_string())
|
||
.unwrap_or_default();
|
||
trace!("Model for {name}: '{}'", model);
|
||
|
||
let serial = device
|
||
.get("serial")
|
||
.and_then(|v| v.as_str())
|
||
.map(|s| s.trim().to_string())
|
||
.unwrap_or_default();
|
||
trace!("Serial for {name}: '{}'", serial);
|
||
|
||
let size_str = device
|
||
.get("size")
|
||
.and_then(|v| v.as_str())
|
||
.ok_or("Missing 'size' in lsblk device")?;
|
||
trace!("Reported size for {name}: {}", size_str);
|
||
let size_bytes = Self::parse_size(size_str)?;
|
||
trace!("Parsed size for {name}: {} bytes", size_bytes);
|
||
|
||
let rotational = device
|
||
.get("rota")
|
||
.and_then(|v| v.as_bool())
|
||
.ok_or("Missing 'rota' in lsblk device")?;
|
||
trace!("Rotational flag for {name}: {}", rotational);
|
||
|
||
let wwn = device
|
||
.get("wwn")
|
||
.and_then(|v| v.as_str())
|
||
.map(|s| s.trim().to_string())
|
||
.filter(|s| !s.is_empty() && s != "null");
|
||
trace!("WWN for {name}: {:?}", wwn);
|
||
|
||
let device_path = Path::new("/sys/block").join(&name);
|
||
trace!("Sysfs path for {name}: {:?}", device_path);
|
||
|
||
trace!("Reading logical block size for {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))?;
|
||
trace!("Logical block size for {name}: {}", logical_block_size);
|
||
|
||
trace!("Reading physical block size for {name}");
|
||
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))?;
|
||
trace!("Physical block size for {name}: {}", physical_block_size);
|
||
|
||
trace!("Determining interface type for {name}");
|
||
let interface_type = Self::get_interface_type(&name, &device_path)?;
|
||
trace!("Interface type for {name}: {}", interface_type);
|
||
|
||
trace!("Getting SMART status for {name}");
|
||
let smart_status = Self::get_smart_status(&name)?;
|
||
trace!("SMART status for {name}: {:?}", smart_status);
|
||
|
||
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() {
|
||
trace!("Enhancing drive {name} with extra sysfs metadata");
|
||
if drive.model.is_empty() {
|
||
trace!("Reading model from sysfs for {name}");
|
||
drive.model = Self::read_sysfs_string(&device_path.join("device/model"))
|
||
.unwrap_or_else(|_| format!("Failed to read model for {}", name));
|
||
}
|
||
if drive.serial.is_empty() {
|
||
trace!("Reading serial from sysfs for {name}");
|
||
drive.serial = Self::read_sysfs_string(&device_path.join("device/serial"))
|
||
.unwrap_or_else(|_| format!("Failed to read serial for {}", name));
|
||
}
|
||
} else {
|
||
trace!(
|
||
"Sysfs path {:?} not found for drive {name}, skipping extra metadata",
|
||
device_path
|
||
);
|
||
}
|
||
|
||
debug!("Discovered storage drive: {drive:?}");
|
||
drives.push(drive);
|
||
}
|
||
|
||
debug!("Discovered total {} storage drives", drives.len());
|
||
trace!("All discovered dives: {drives:?}");
|
||
|
||
Ok(drives)
|
||
}
|
||
|
||
fn gather_storage_controller() -> Result<StorageController, String> {
|
||
let mut controller = StorageController {
|
||
name: "Unknown".to_string(),
|
||
driver: "Unknown".to_string(),
|
||
};
|
||
|
||
// Use lspci with JSON output if available
|
||
let output = Command::new("lspci")
|
||
.args(["-nn", "-d", "::0100", "-J"]) // Storage controllers class with JSON
|
||
.output()
|
||
.map_err(|e| format!("Failed to execute lspci: {}", e))?;
|
||
|
||
if output.status.success() {
|
||
let json: Value = serde_json::from_slice(&output.stdout)
|
||
.map_err(|e| format!("Failed to parse lspci JSON output: {}", e))?;
|
||
|
||
if 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 or no device found
|
||
if controller.name == "Unknown" {
|
||
let output = Command::new("lspci")
|
||
.args(["-nn", "-d", "::0100"]) // Storage controllers class
|
||
.output()
|
||
.map_err(|e| format!("Failed to execute lspci (fallback): {}", e))?;
|
||
|
||
if 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
|
||
let output = Command::new("lsmod")
|
||
.output()
|
||
.map_err(|e| format!("Failed to execute lsmod: {}", e))?;
|
||
|
||
if 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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
debug!("Found storage controller {controller:?}");
|
||
|
||
Ok(controller)
|
||
}
|
||
|
||
fn gather_memory_modules() -> Result<Vec<MemoryModule>, String> {
|
||
let mut modules = Vec::new();
|
||
|
||
let output = Command::new("dmidecode")
|
||
.arg("--type")
|
||
.arg("17")
|
||
.output()
|
||
.map_err(|e| format!("Failed to execute dmidecode: {}", e))?;
|
||
|
||
if !output.status.success() {
|
||
return Err(format!(
|
||
"dmidecode command failed: {}",
|
||
String::from_utf8_lossy(&output.stderr)
|
||
));
|
||
}
|
||
|
||
let output_str = String::from_utf8(output.stdout)
|
||
.map_err(|e| format!("Failed to parse dmidecode output: {}", e))?;
|
||
|
||
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::<u64>()
|
||
{
|
||
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);
|
||
}
|
||
}
|
||
|
||
debug!("Found memory modules {modules:?}");
|
||
Ok(modules)
|
||
}
|
||
|
||
fn gather_cpus(sys: &System) -> Result<Vec<CPU>, String> {
|
||
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(),
|
||
});
|
||
|
||
debug!("Found cpus {cpus:?}");
|
||
|
||
Ok(cpus)
|
||
}
|
||
|
||
fn gather_chipset() -> Result<Chipset, String> {
|
||
let chipset = Chipset {
|
||
name: Self::read_dmi("baseboard-product-name")?,
|
||
vendor: Self::read_dmi("baseboard-manufacturer")?,
|
||
};
|
||
|
||
debug!("Found chipset {chipset:?}");
|
||
|
||
Ok(chipset)
|
||
}
|
||
|
||
fn gather_network_interfaces() -> Result<Vec<NetworkInterface>, String> {
|
||
let mut interfaces = Vec::new();
|
||
let sys_net_path = Path::new("/sys/class/net");
|
||
trace!("Reading /sys/class/net");
|
||
|
||
let entries = fs::read_dir(sys_net_path)
|
||
.map_err(|e| format!("Failed to read /sys/class/net: {}", e))?;
|
||
trace!("Got entries {entries:?}");
|
||
|
||
for entry in entries {
|
||
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
|
||
let iface_name = entry
|
||
.file_name()
|
||
.into_string()
|
||
.map_err(|_| "Invalid UTF-8 in interface name")?;
|
||
let iface_path = entry.path();
|
||
trace!("Inspecting interface {iface_name} path {iface_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")
|
||
{
|
||
trace!(
|
||
"Skipping interface {iface_name} because it appears to be virtual/unsupported"
|
||
);
|
||
continue;
|
||
}
|
||
|
||
// Check if it's a physical interface by looking for device directory
|
||
if !iface_path.join("device").exists() {
|
||
trace!(
|
||
"Skipping interface {iface_name} since {iface_path:?}/device does not exist"
|
||
);
|
||
continue;
|
||
}
|
||
|
||
trace!("Reading MAC address for {iface_name}");
|
||
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 mac_address = MacAddress::try_from(mac_address).map_err(|e| e.to_string())?;
|
||
trace!("MAC address for {iface_name}: {mac_address}");
|
||
|
||
let speed_path = iface_path.join("speed");
|
||
let speed_mbps = if speed_path.exists() {
|
||
trace!("Reading speed for {iface_name} from {:?}", speed_path);
|
||
match Self::read_sysfs_u32(&speed_path) {
|
||
Ok(speed) => {
|
||
trace!("Speed for {iface_name}: {speed} Mbps");
|
||
Some(speed)
|
||
}
|
||
Err(e) => {
|
||
debug!(
|
||
"Failed to read speed for {}: {} (this may be expected on Wi‑Fi interfaces)",
|
||
iface_name, e
|
||
);
|
||
None
|
||
}
|
||
}
|
||
} else {
|
||
trace!("Speed file not found for {iface_name}, skipping");
|
||
None
|
||
};
|
||
|
||
trace!("Reading operstate for {iface_name}");
|
||
let operstate = Self::read_sysfs_string(&iface_path.join("operstate"))
|
||
.map_err(|e| format!("Failed to read operstate for {}: {}", iface_name, e))?;
|
||
trace!("Operstate for {iface_name}: {operstate}");
|
||
|
||
trace!("Reading MTU for {iface_name}");
|
||
let mtu = Self::read_sysfs_u32(&iface_path.join("mtu"))
|
||
.map_err(|e| format!("Failed to read MTU for {}: {}", iface_name, e))?;
|
||
trace!("MTU for {iface_name}: {mtu}");
|
||
|
||
trace!("Reading driver for {iface_name}");
|
||
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))?;
|
||
trace!("Driver for {iface_name}: {driver}");
|
||
|
||
trace!("Reading firmware version for {iface_name}");
|
||
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))?;
|
||
trace!("Firmware version for {iface_name}: {firmware_version:?}");
|
||
|
||
trace!("Fetching IP addresses for {iface_name}");
|
||
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))?;
|
||
trace!("Interface {iface_name} has IPv4: {ipv4_addresses:?}, IPv6: {ipv6_addresses:?}");
|
||
|
||
let is_up = operstate == "up";
|
||
trace!("Constructing NetworkInterface for {iface_name} (is_up={is_up})");
|
||
|
||
let iface = NetworkInterface {
|
||
name: iface_name.clone(),
|
||
mac_address,
|
||
speed_mbps,
|
||
is_up,
|
||
mtu,
|
||
ipv4_addresses,
|
||
ipv6_addresses,
|
||
driver,
|
||
firmware_version,
|
||
};
|
||
|
||
debug!("Discovered interface: {iface:?}");
|
||
interfaces.push(iface);
|
||
}
|
||
|
||
debug!("Discovered total {} network interfaces", interfaces.len());
|
||
trace!("Interfaces collected: {interfaces:?}");
|
||
Ok(interfaces)
|
||
}
|
||
|
||
fn gather_management_interface() -> Result<Option<ManagementInterface>, String> {
|
||
let mgmt = if Path::new("/dev/ipmi0").exists() {
|
||
Ok(Some(ManagementInterface {
|
||
kind: "IPMI".to_string(),
|
||
address: None,
|
||
firmware: Some(Self::read_dmi("bios-version")?),
|
||
}))
|
||
} else if Path::new("/sys/class/misc/mei").exists() {
|
||
Ok(Some(ManagementInterface {
|
||
kind: "Intel ME".to_string(),
|
||
address: None,
|
||
firmware: None,
|
||
}))
|
||
} else {
|
||
Ok(None)
|
||
};
|
||
|
||
debug!("Found management interface {mgmt:?}");
|
||
mgmt
|
||
}
|
||
|
||
fn get_host_uuid() -> Result<String, String> {
|
||
let uuid = Self::read_dmi("system-uuid");
|
||
debug!("Found uuid {uuid:?}");
|
||
uuid
|
||
}
|
||
|
||
// Helper methods
|
||
fn read_sysfs_string(path: &Path) -> Result<String, String> {
|
||
fs::read_to_string(path)
|
||
.map(|s| s.trim().to_string())
|
||
.map_err(|e| format!("Failed to read {}: {}", path.display(), e))
|
||
}
|
||
|
||
fn read_sysfs_opt_string(path: &Path) -> Result<Option<String>, String> {
|
||
match fs::read_to_string(path) {
|
||
Ok(s) => {
|
||
let s = s.trim().to_string();
|
||
Ok(if s.is_empty() { None } else { Some(s) })
|
||
}
|
||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||
Err(e) => Err(format!("Failed to read {}: {}", path.display(), e)),
|
||
}
|
||
}
|
||
|
||
fn read_sysfs_u32(path: &Path) -> Result<u32, String> {
|
||
fs::read_to_string(path)
|
||
.map_err(|e| format!("Failed to read {}: {}", path.display(), e))?
|
||
.trim()
|
||
.parse()
|
||
.map_err(|e| format!("Failed to parse {}: {}", path.display(), e))
|
||
}
|
||
|
||
fn read_sysfs_symlink_basename(path: &Path) -> Result<String, String> {
|
||
match fs::read_link(path) {
|
||
Ok(target_path) => match target_path.file_name() {
|
||
Some(name_osstr) => match name_osstr.to_str() {
|
||
Some(name_str) => Ok(name_str.to_string()),
|
||
None => Err(format!(
|
||
"Symlink target basename is not valid UTF-8: {}",
|
||
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 read_dmi(field: &str) -> Result<String, String> {
|
||
let output = Command::new("dmidecode")
|
||
.arg("-s")
|
||
.arg(field)
|
||
.output()
|
||
.map_err(|e| format!("Failed to execute dmidecode for field {}: {}", field, e))?;
|
||
|
||
if !output.status.success() {
|
||
return Err(format!(
|
||
"dmidecode command failed for field {}: {}",
|
||
field,
|
||
String::from_utf8_lossy(&output.stderr)
|
||
));
|
||
}
|
||
|
||
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 get_interface_type(device_name: &str, device_path: &Path) -> Result<String, String> {
|
||
if device_name.starts_with("nvme") {
|
||
Ok("NVMe".to_string())
|
||
} else if device_name.starts_with("sd") {
|
||
Ok("SATA".to_string())
|
||
} else if device_name.starts_with("hd") {
|
||
Ok("IDE".to_string())
|
||
} else if device_name.starts_with("vd") {
|
||
Ok("VirtIO".to_string())
|
||
} else if device_name.starts_with("sr") {
|
||
Ok("CDROM".to_string())
|
||
} else if device_name.starts_with("zram") {
|
||
Ok("Ramdisk".to_string())
|
||
} else {
|
||
// Try to determine from device path
|
||
let subsystem = Self::read_sysfs_string(&device_path.join("device/subsystem"))
|
||
.unwrap_or(String::new());
|
||
Ok(subsystem
|
||
.split('/')
|
||
.next_back()
|
||
.unwrap_or("Unknown")
|
||
.to_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)
|
||
}
|
||
|
||
// FIXME when scanning an interface that is part of a bond/bridge we won't get an address on the
|
||
// interface, we should be looking at the bond/bridge device. For example, br-ex on k8s nodes.
|
||
fn get_interface_ips_json(iface_name: &str) -> Result<(Vec<String>, Vec<String>), String> {
|
||
let mut ipv4 = Vec::new();
|
||
let mut ipv6 = Vec::new();
|
||
|
||
// Get IPv4 addresses using JSON output
|
||
let output = Command::new("ip")
|
||
.args(["-j", "-4", "addr", "show", iface_name])
|
||
.output()
|
||
.map_err(|e| {
|
||
format!(
|
||
"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 {
|
||
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
|
||
let output = Command::new("ip")
|
||
.args(["-j", "-6", "addr", "show", iface_name])
|
||
.output()
|
||
.map_err(|e| {
|
||
format!(
|
||
"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 {
|
||
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());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok((ipv4, ipv6))
|
||
}
|
||
}
|