Some checks failed
Run Check Script / check (pull_request) Failing after 29s
570 lines
22 KiB
Rust
570 lines
22 KiB
Rust
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)]
|
|
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>,
|
|
}
|
|
|
|
#[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<u32>,
|
|
pub manufacturer: Option<String>,
|
|
pub part_number: Option<String>,
|
|
pub serial_number: Option<String>,
|
|
pub rank: Option<u8>,
|
|
}
|
|
|
|
#[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<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>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
pub struct ManagementInterface {
|
|
pub kind: String,
|
|
pub address: Option<String>,
|
|
pub firmware: Option<String>,
|
|
}
|
|
|
|
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<StorageDrive> {
|
|
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::<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
|
|
.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::<Value>(&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<MemoryModule> {
|
|
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::<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);
|
|
}
|
|
}
|
|
}
|
|
|
|
modules
|
|
}
|
|
|
|
fn gather_cpus(sys: &System) -> Vec<CPU> {
|
|
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<NetworkInterface> {
|
|
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<ManagementInterface> {
|
|
// 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<String> {
|
|
fs::read_to_string(path)
|
|
.ok()
|
|
.map(|s| s.trim().to_string())
|
|
.filter(|s| !s.is_empty())
|
|
}
|
|
|
|
fn read_sysfs_u32(path: &Path) -> Option<u32> {
|
|
fs::read_to_string(path)
|
|
.ok()
|
|
.and_then(|s| s.trim().parse().ok())
|
|
}
|
|
|
|
fn read_dmi(field: &str) -> Option<String> {
|
|
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<String> {
|
|
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<u64> {
|
|
if size_str.ends_with('T') {
|
|
size_str[..size_str.len() - 1]
|
|
.parse::<u64>()
|
|
.ok()
|
|
.map(|t| t * 1024 * 1024 * 1024 * 1024)
|
|
} else if size_str.ends_with('G') {
|
|
size_str[..size_str.len() - 1]
|
|
.parse::<u64>()
|
|
.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 {
|
|
size_str.parse::<u64>().ok()
|
|
}
|
|
}
|
|
|
|
fn get_interface_ips_json(iface_name: &str) -> (Vec<String>, Vec<String>) {
|
|
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::<Value>(&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::<Value>(&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)
|
|
}
|
|
}
|