feat(inventory): Fully automated inventory gathering now works!! Been waiting a long time for this feature
Boot up harmony_inventory_agent with `cargo run -p harmony_inventory_agent` Launch the DiscoverInventoryAgentScore , currently available this way : `RUST_LOG=info cargo run -p example-cli -- -f Discover -y` And you will have automatically all hosts saved to the database. Run `cargo sqlx setup` if you have not done it yet.
This commit is contained in:
		
							parent
							
								
									d9c26f43ee
								
							
						
					
					
						commit
						637ffde992
					
				| @ -3,7 +3,7 @@ use std::sync::Arc; | ||||
| use derive_new::new; | ||||
| use harmony_inventory_agent::hwinfo::{CPU, MemoryModule, NetworkInterface, StorageDrive}; | ||||
| use harmony_types::net::MacAddress; | ||||
| use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use serde_value::Value; | ||||
| 
 | ||||
| pub type HostGroup = Vec<PhysicalHost>; | ||||
| @ -35,7 +35,120 @@ impl PhysicalHost { | ||||
|     } | ||||
| 
 | ||||
|     pub fn summary(&self) -> String { | ||||
|         todo!(); | ||||
|         let mut parts = Vec::new(); | ||||
| 
 | ||||
|         // Part 1: System Model (from labels) or Category as a fallback
 | ||||
|         let model = self | ||||
|             .labels | ||||
|             .iter() | ||||
|             .find(|l| l.name == "system-product-name" || l.name == "model") | ||||
|             .map(|l| l.value.clone()) | ||||
|             .unwrap_or_else(|| self.category.to_string()); | ||||
|         parts.push(model); | ||||
| 
 | ||||
|         // Part 2: CPU Information
 | ||||
|         if !self.cpus.is_empty() { | ||||
|             let cpu_count = self.cpus.len(); | ||||
|             let total_cores = self.cpus.iter().map(|c| c.cores).sum::<u32>(); | ||||
|             let total_threads = self.cpus.iter().map(|c| c.threads).sum::<u32>(); | ||||
|             let model_name = &self.cpus[0].model; | ||||
| 
 | ||||
|             let cpu_summary = if cpu_count > 1 { | ||||
|                 format!( | ||||
|                     "{}x {} ({}c/{}t)", | ||||
|                     cpu_count, model_name, total_cores, total_threads | ||||
|                 ) | ||||
|             } else { | ||||
|                 format!("{} ({}c/{}t)", model_name, total_cores, total_threads) | ||||
|             }; | ||||
|             parts.push(cpu_summary); | ||||
|         } | ||||
| 
 | ||||
|         // Part 3: Memory Information
 | ||||
|         if !self.memory_modules.is_empty() { | ||||
|             let total_mem_bytes = self | ||||
|                 .memory_modules | ||||
|                 .iter() | ||||
|                 .map(|m| m.size_bytes) | ||||
|                 .sum::<u64>(); | ||||
|             let total_mem_gb = (total_mem_bytes as f64 / (1024.0 * 1024.0 * 1024.0)).round() as u64; | ||||
| 
 | ||||
|             // Find the most common speed among modules
 | ||||
|             let mut speeds = std::collections::HashMap::new(); | ||||
|             for module in &self.memory_modules { | ||||
|                 if let Some(speed) = module.speed_mhz { | ||||
|                     *speeds.entry(speed).or_insert(0) += 1; | ||||
|                 } | ||||
|             } | ||||
|             let common_speed = speeds | ||||
|                 .into_iter() | ||||
|                 .max_by_key(|&(_, count)| count) | ||||
|                 .map(|(speed, _)| speed); | ||||
| 
 | ||||
|             if let Some(speed) = common_speed { | ||||
|                 parts.push(format!("{} GB RAM @ {}MHz", total_mem_gb, speed)); | ||||
|             } else { | ||||
|                 parts.push(format!("{} GB RAM", total_mem_gb)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Part 4: Storage Information
 | ||||
|         if !self.storage.is_empty() { | ||||
|             let total_storage_bytes = self.storage.iter().map(|d| d.size_bytes).sum::<u64>(); | ||||
|             let drive_count = self.storage.len(); | ||||
|             let first_drive_model = &self.storage[0].model; | ||||
| 
 | ||||
|             // Helper to format bytes into TB or GB
 | ||||
|             let format_storage = |bytes: u64| { | ||||
|                 let tb = bytes as f64 / (1024.0 * 1024.0 * 1024.0 * 1024.0); | ||||
|                 if tb >= 1.0 { | ||||
|                     format!("{:.2} TB", tb) | ||||
|                 } else { | ||||
|                     let gb = bytes as f64 / (1024.0 * 1024.0 * 1024.0); | ||||
|                     format!("{:.0} GB", gb) | ||||
|                 } | ||||
|             }; | ||||
| 
 | ||||
|             let storage_summary = if drive_count > 1 { | ||||
|                 format!( | ||||
|                     "{} Storage ({}x {})", | ||||
|                     format_storage(total_storage_bytes), | ||||
|                     drive_count, | ||||
|                     first_drive_model | ||||
|                 ) | ||||
|             } else { | ||||
|                 format!( | ||||
|                     "{} Storage ({})", | ||||
|                     format_storage(total_storage_bytes), | ||||
|                     first_drive_model | ||||
|                 ) | ||||
|             }; | ||||
|             parts.push(storage_summary); | ||||
|         } | ||||
| 
 | ||||
|         // Part 5: Network Information
 | ||||
|         // Prioritize an "up" interface with an IPv4 address
 | ||||
|         let best_nic = self | ||||
|             .network | ||||
|             .iter() | ||||
|             .find(|n| n.is_up && !n.ipv4_addresses.is_empty()) | ||||
|             .or_else(|| self.network.first()); | ||||
| 
 | ||||
|         if let Some(nic) = best_nic { | ||||
|             let speed = nic | ||||
|                 .speed_mbps | ||||
|                 .map(|s| format!("{}Gbps", s / 1000)) | ||||
|                 .unwrap_or_else(|| "N/A".to_string()); | ||||
|             let mac = nic.mac_address.to_string(); | ||||
|             let nic_summary = if let Some(ip) = nic.ipv4_addresses.first() { | ||||
|                 format!("NIC: {} ({}, {})", speed, ip, mac) | ||||
|             } else { | ||||
|                 format!("NIC: {} ({})", speed, mac) | ||||
|             }; | ||||
|             parts.push(nic_summary); | ||||
|         } | ||||
| 
 | ||||
|         parts.join(" | ") | ||||
|     } | ||||
| 
 | ||||
|     pub fn cluster_mac(&self) -> MacAddress { | ||||
|  | ||||
| @ -71,7 +71,6 @@ impl<T: Topology> Interpret<T> for DiscoverInventoryAgentInterpret { | ||||
|                         info!("Getting host inventory on service at {address} port {port}"); | ||||
| 
 | ||||
|                         tokio::task::spawn(async move { | ||||
|                             todo!("are we here"); | ||||
|                             info!("Getting inventory for host {address} {port}"); | ||||
|                             let host = | ||||
|                                 harmony_inventory_agent::client::get_host_inventory(&address, port) | ||||
| @ -126,20 +125,9 @@ impl<T: Topology> Interpret<T> for DiscoverInventoryAgentInterpret { | ||||
|             }, | ||||
|         ) | ||||
|         .await; | ||||
|         info!("Launched inventory host information gathering"); | ||||
|         info!( | ||||
|             "tokio current {:?}", | ||||
|             tokio::runtime::Handle::try_current().unwrap() | ||||
|         ); | ||||
|         tokio::spawn(async { | ||||
|             info!("Spawned a sleeper"); | ||||
|             tokio::time::sleep(Duration::from_millis(100)).await; | ||||
|             info!("done a sleeper"); | ||||
|         }); | ||||
|         tokio::time::sleep(Duration::from_millis(1000)).await; | ||||
|         Ok(Outcome { | ||||
|             status: InterpretStatus::RUNNING, | ||||
|             message: "Launched discovery process".to_string(), | ||||
|             status: InterpretStatus::SUCCESS, | ||||
|             message: "Discovery process completed successfully".to_string(), | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -16,7 +16,7 @@ where | ||||
|     // The receiver will be a stream of events.
 | ||||
|     let receiver = mdns.browse(SERVICE_NAME).expect("Failed to browse"); | ||||
| 
 | ||||
|     tokio::spawn(async move { | ||||
|     tokio::task::spawn_blocking(move || { | ||||
|         while let Ok(event) = receiver.recv() { | ||||
|             if let Err(e) = on_event(event.clone()) { | ||||
|                 error!("Event callback failed : {e}"); | ||||
|  | ||||
							
								
								
									
										8
									
								
								migrations/20250830163356_Physical_hosts.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								migrations/20250830163356_Physical_hosts.sql
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| -- Add migration script here | ||||
| CREATE TABLE IF NOT EXISTS physical_hosts ( | ||||
|     version_id TEXT PRIMARY KEY NOT NULL, | ||||
|     id TEXT NOT NULL, | ||||
|     data JSON NOT NULL | ||||
| ); | ||||
| CREATE INDEX IF NOT EXISTS idx_host_id_time | ||||
| ON physical_hosts (id, version_id DESC); | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user