diff --git a/Cargo.lock b/Cargo.lock index 42c4cd2..12f1af0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,189 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44dfe5c9e0004c623edc65391dfd51daa201e7e30ebd9c9bedf873048ec32bc2" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "base64 0.22.1", + "bitflags 2.9.1", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "foldhash", + "futures-core", + "h2 0.3.26", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.9.1", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http 0.2.12", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio 1.0.4", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "foldhash", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "tracing", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -75,6 +258,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -398,6 +596,27 @@ dependencies = [ "serde_with", ] +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bstr" version = "1.12.0" @@ -427,6 +646,15 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "bytestring" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" +dependencies = [ + "bytes", +] + [[package]] name = "camino" version = "1.1.10" @@ -506,6 +734,8 @@ version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -704,6 +934,17 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -757,6 +998,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -967,6 +1227,7 @@ dependencies = [ "proc-macro2", "quote", "syn", + "unicode-xid", ] [[package]] @@ -1869,6 +2130,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "harmony_inventory_agent" +version = "0.1.0" +dependencies = [ + "actix-web", + "env_logger", + "log", + "serde", + "serde_json", + "sysinfo", + "uuid", +] + [[package]] name = "harmony_macros" version = "0.1.0" @@ -2307,7 +2581,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.61.2", ] [[package]] @@ -2432,6 +2706,12 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "impl-more" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" + [[package]] name = "indenter" version = "0.3.3" @@ -2590,6 +2870,16 @@ dependencies = [ "syn", ] +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -2792,6 +3082,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + [[package]] name = "lazy_static" version = "1.5.0" @@ -2855,6 +3151,23 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + [[package]] name = "lock_api" version = "0.4.13" @@ -2984,6 +3297,15 @@ dependencies = [ "serde", ] +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -3704,6 +4026,26 @@ dependencies = [ "unicode-width 0.2.0", ] +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.13" @@ -3767,6 +4109,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + [[package]] name = "regex-syntax" version = "0.8.5" @@ -4799,6 +5147,21 @@ dependencies = [ "syn", ] +[[package]] +name = "sysinfo" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -4998,6 +5361,7 @@ dependencies = [ "bytes", "libc", "mio 1.0.4", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -5575,6 +5939,25 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -6058,3 +6441,31 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index b12a4b5..b85bd72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "harmony_cli", "k3d", "harmony_composer", + "harmony_inventory_agent", ] [workspace.package] @@ -56,3 +57,8 @@ pretty_assertions = "1.4.1" bollard = "0.19.1" base64 = "0.22.1" tar = "0.4.44" +lazy_static = "1.5.0" +directories = "6.0.0" +thiserror = "2.0.14" +serde = { version = "1.0.209", features = ["derive", "rc"] } +serde_json = "1.0.127" diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index 5a42cf7..9274de4 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -16,8 +16,8 @@ reqwest = { version = "0.11", features = ["blocking", "json"] } russh = "0.45.0" rust-ipmi = "0.1.1" semver = "1.0.23" -serde = { version = "1.0.209", features = ["derive", "rc"] } -serde_json = "1.0.127" +serde.workspace = true +serde_json.workspace = true tokio.workspace = true derive-new.workspace = true log.workspace = true diff --git a/harmony_inventory_agent/Cargo.toml b/harmony_inventory_agent/Cargo.toml new file mode 100644 index 0000000..a299ca0 --- /dev/null +++ b/harmony_inventory_agent/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "harmony_inventory_agent" +version = "0.1.0" +edition = "2024" + +[dependencies] +actix-web = "4.4" +sysinfo = "0.30" +serde.workspace = true +serde_json.workspace = true +log.workspace = true +env_logger.workspace = true +uuid.workspace = true diff --git a/harmony_inventory_agent/src/hwinfo.rs b/harmony_inventory_agent/src/hwinfo.rs new file mode 100644 index 0000000..5cea184 --- /dev/null +++ b/harmony_inventory_agent/src/hwinfo.rs @@ -0,0 +1,569 @@ +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) + } +} diff --git a/harmony_inventory_agent/src/main.rs b/harmony_inventory_agent/src/main.rs new file mode 100644 index 0000000..a938c69 --- /dev/null +++ b/harmony_inventory_agent/src/main.rs @@ -0,0 +1,29 @@ +// src/main.rs +use actix_web::{App, HttpServer, Responder, get}; +use hwinfo::PhysicalHost; +use std::env; + +mod hwinfo; + +#[get("/inventory")] +async fn inventory() -> impl Responder { + log::info!("Received inventory request"); + let host = PhysicalHost::gather(); + log::info!("Inventory data gathered successfully"); + actix_web::HttpResponse::Ok().json(host) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + env_logger::init(); + + let port = env::var("PORT").unwrap_or_else(|_| "8080".to_string()); + let bind_addr = format!("0.0.0.0:{}", port); + + log::info!("Starting inventory agent on {}", bind_addr); + + HttpServer::new(|| App::new().service(inventory)) + .bind(&bind_addr)? + .run() + .await +}