feat: Can now discover inventory agent and download its host definition, next up save it to db
This commit is contained in:
		
							parent
							
								
									f9906cb419
								
							
						
					
					
						commit
						e548bf619a
					
				
							
								
								
									
										3
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -2366,9 +2366,12 @@ version = "0.1.0" | ||||
| dependencies = [ | ||||
|  "actix-web", | ||||
|  "env_logger", | ||||
|  "harmony_macros", | ||||
|  "harmony_types", | ||||
|  "local-ip-address", | ||||
|  "log", | ||||
|  "mdns-sd 0.14.1 (git+https://github.com/jggc/mdns-sd.git?branch=patch-1)", | ||||
|  "reqwest 0.12.20", | ||||
|  "serde", | ||||
|  "serde_json", | ||||
|  "sysinfo", | ||||
|  | ||||
| @ -67,4 +67,4 @@ serde = { version = "1.0.209", features = ["derive", "rc"] } | ||||
| serde_json = "1.0.127" | ||||
| askama = "0.14" | ||||
| sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite" ] } | ||||
| reqwest = { version = "0.12", features = ["stream", "rustls-tls", "http2"], default-features = false } | ||||
| reqwest = { version = "0.12", features = ["blocking", "stream", "rustls-tls", "http2", "json"], default-features = false } | ||||
|  | ||||
| @ -4,6 +4,7 @@ use derive_new::new; | ||||
| use harmony_types::net::MacAddress; | ||||
| use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct}; | ||||
| use serde_value::Value; | ||||
| use harmony_inventory_agent::hwinfo::NetworkInterface; | ||||
| 
 | ||||
| pub type HostGroup = Vec<PhysicalHost>; | ||||
| pub type SwitchGroup = Vec<Switch>; | ||||
| @ -70,9 +71,15 @@ impl PhysicalHost { | ||||
| 
 | ||||
|     pub fn mac_address(mut self, mac_address: MacAddress) -> Self { | ||||
|         self.network.push(NetworkInterface { | ||||
|             name: None, | ||||
|             name: String::new(), | ||||
|             mac_address, | ||||
|             speed: None, | ||||
|             speed_mbps: None, | ||||
|             is_up: false, | ||||
|             mtu: 0, | ||||
|             ipv4_addresses: vec![], | ||||
|             ipv6_addresses: vec![], | ||||
|             driver: String::new(), | ||||
|             firmware_version: None, | ||||
|         }); | ||||
|         self | ||||
|     } | ||||
| @ -131,7 +138,7 @@ impl Serialize for PhysicalHost { | ||||
| } | ||||
| 
 | ||||
| impl<'de> Deserialize<'de> for PhysicalHost { | ||||
|     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> | ||||
|     fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error> | ||||
|     where | ||||
|         D: serde::Deserializer<'de>, | ||||
|     { | ||||
| @ -189,28 +196,10 @@ pub enum HostCategory { | ||||
|     Switch, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, new, Clone, Serialize)] | ||||
| pub struct NetworkInterface { | ||||
|     pub name: Option<String>, | ||||
|     pub mac_address: MacAddress, | ||||
|     pub speed: Option<u64>, | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| use harmony_macros::mac_address; | ||||
| 
 | ||||
| use harmony_types::id::Id; | ||||
| #[cfg(test)] | ||||
| impl NetworkInterface { | ||||
|     pub fn dummy() -> Self { | ||||
|         Self { | ||||
|             name: Some(String::new()), | ||||
|             mac_address: mac_address!("00:00:00:00:00:00"), | ||||
|             speed: Some(0), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, new, Clone, Serialize)] | ||||
| pub enum StorageConnectionType { | ||||
|     Sata3g, | ||||
|  | ||||
| @ -15,16 +15,15 @@ pub struct SqliteInventoryRepository { | ||||
| 
 | ||||
| impl SqliteInventoryRepository { | ||||
|     pub async fn new(database_url: &str) -> Result<Self, RepoError> { | ||||
|         let pool = SqlitePool::connect(database_url) | ||||
|         let _pool = SqlitePool::connect(database_url) | ||||
|             .await | ||||
|             .map_err(|e| RepoError::ConnectionFailed(e.to_string()))?; | ||||
| 
 | ||||
|         todo!("make sure migrations are up to date"); | ||||
|         info!( | ||||
|             "SQLite inventory repository initialized at '{}'", | ||||
|             database_url, | ||||
|         ); | ||||
|         Ok(Self { pool }) | ||||
|         Ok(Self { pool: _pool }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -50,7 +49,7 @@ impl InventoryRepository for SqliteInventoryRepository { | ||||
|     } | ||||
| 
 | ||||
|     async fn get_latest_by_id(&self, host_id: &str) -> Result<Option<PhysicalHost>, RepoError> { | ||||
|         let row = sqlx::query_as!( | ||||
|         let _row = sqlx::query_as!( | ||||
|             DbHost, | ||||
|             r#"SELECT id, version_id, data as "data: Json<PhysicalHost>" FROM physical_hosts WHERE id = ? ORDER BY version_id DESC LIMIT 1"#, | ||||
|             host_id | ||||
|  | ||||
| @ -1,10 +1,11 @@ | ||||
| use async_trait::async_trait; | ||||
| use harmony_inventory_agent::local_presence::DiscoveryEvent; | ||||
| use log::{debug, info}; | ||||
| use log::{debug, info, trace}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| use crate::{ | ||||
|     data::Version, | ||||
|     hardware::{HostCategory, PhysicalHost}, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     score::Score, | ||||
| @ -41,17 +42,50 @@ struct DiscoverInventoryAgentInterpret { | ||||
| impl<T: Topology> Interpret<T> for DiscoverInventoryAgentInterpret { | ||||
|     async fn execute( | ||||
|         &self, | ||||
|         inventory: &Inventory, | ||||
|         topology: &T, | ||||
|         _inventory: &Inventory, | ||||
|         _topology: &T, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         harmony_inventory_agent::local_presence::discover_agents( | ||||
|             self.score.discovery_timeout, | ||||
|             |event: DiscoveryEvent| { | ||||
|             |event: DiscoveryEvent| -> Result<(), String> { | ||||
|                 println!("Discovery event {event:?}"); | ||||
|                 match event { | ||||
|                     DiscoveryEvent::ServiceResolved(service) => info!("Found instance {service:?}"), | ||||
|                     DiscoveryEvent::ServiceResolved(service) => { | ||||
|                         debug!("Found instance {service:?}"); | ||||
|                         let address = match service.get_addresses().iter().next() { | ||||
|                             Some(address) => address, | ||||
|                             None => { | ||||
|                                 return Err( | ||||
|                                     "Could not find address for service {service:?}".to_string() | ||||
|                                 ); | ||||
|                             } | ||||
|                         }; | ||||
| 
 | ||||
|                         let address = &address.to_string(); | ||||
|                         let port = service.get_port(); | ||||
| 
 | ||||
|                         debug!("Getting host inventory on service at {address} port {port}"); | ||||
| 
 | ||||
|                         let host = | ||||
|                             harmony_inventory_agent::client::get_host_inventory(address, port)?; | ||||
| 
 | ||||
|                         trace!("Found host information {host:?}"); | ||||
|                         // TODO its useless to have two distinct host types but requires a bit much
 | ||||
|                         // refactoring to do it now
 | ||||
|                         let host = PhysicalHost { | ||||
|                             id: Id::from(host.host_uuid), | ||||
|                             category: HostCategory::Server, | ||||
|                             network: todo!(), | ||||
|                             management: todo!(), | ||||
|                             storage: todo!(), | ||||
|                             labels: todo!(), | ||||
|                             memory_size: todo!(), | ||||
|                             cpu_count: todo!(), | ||||
|                         }; | ||||
|                     } | ||||
|                     _ => debug!("Unhandled event {event:?}"), | ||||
|                 } | ||||
|                 }; | ||||
|                 Ok(()) | ||||
|             }, | ||||
|         ); | ||||
|         todo!() | ||||
|  | ||||
| @ -12,6 +12,9 @@ log.workspace = true | ||||
| env_logger.workspace = true | ||||
| tokio.workspace = true | ||||
| thiserror.workspace = true | ||||
| reqwest.workspace = true | ||||
| # mdns-sd = "0.14.1" | ||||
| mdns-sd = { git = "https://github.com/jggc/mdns-sd.git", branch = "patch-1" } | ||||
| local-ip-address = "0.6.5" | ||||
| harmony_types = { path = "../harmony_types" } | ||||
| harmony_macros = { path = "../harmony_macros" } | ||||
|  | ||||
							
								
								
									
										14
									
								
								harmony_inventory_agent/src/client.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								harmony_inventory_agent/src/client.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| use crate::hwinfo::PhysicalHost; | ||||
| 
 | ||||
| pub fn get_host_inventory(host: &str, port: u16) -> Result<PhysicalHost, String> { | ||||
|     let url = format!("http://{host}:{port}/inventory"); | ||||
|     let client = reqwest::blocking::Client::new(); | ||||
|     let response = client | ||||
|         .get(url) | ||||
|         .send() | ||||
|         .map_err(|e| format!("Failed to download file: {e}"))?; | ||||
| 
 | ||||
|     let host = response.json().map_err(|e| e.to_string())?; | ||||
| 
 | ||||
|     Ok(host) | ||||
| } | ||||
| @ -1,3 +1,4 @@ | ||||
| use harmony_types::net::MacAddress; | ||||
| use log::{debug, warn}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use serde_json::Value; | ||||
| @ -63,10 +64,10 @@ pub struct Chipset { | ||||
|     pub vendor: String, | ||||
| } | ||||
| 
 | ||||
| #[derive(Serialize, Deserialize, Debug)] | ||||
| #[derive(Serialize, Deserialize, Debug, Clone)] | ||||
| pub struct NetworkInterface { | ||||
|     pub name: String, | ||||
|     pub mac_address: String, | ||||
|     pub mac_address: MacAddress, | ||||
|     pub speed_mbps: Option<u32>, | ||||
|     pub is_up: bool, | ||||
|     pub mtu: u32, | ||||
| @ -76,6 +77,25 @@ pub struct NetworkInterface { | ||||
|     pub firmware_version: Option<String>, | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| 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, | ||||
| @ -509,6 +529,7 @@ impl PhysicalHost { | ||||
| 
 | ||||
|             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())?; | ||||
| 
 | ||||
|             let speed_mbps = if iface_path.join("speed").exists() { | ||||
|                 match Self::read_sysfs_u32(&iface_path.join("speed")) { | ||||
|  | ||||
| @ -1,2 +1,3 @@ | ||||
| mod hwinfo; | ||||
| pub mod hwinfo; | ||||
| pub mod local_presence; | ||||
| pub mod client; | ||||
|  | ||||
| @ -1,10 +1,14 @@ | ||||
| use log::{debug, error}; | ||||
| use mdns_sd::{ServiceDaemon, ServiceEvent}; | ||||
| 
 | ||||
| use crate::local_presence::SERVICE_NAME; | ||||
| 
 | ||||
| pub type DiscoveryEvent = ServiceEvent; | ||||
| 
 | ||||
| pub fn discover_agents(timeout: Option<u64>, on_event: impl Fn(DiscoveryEvent) + Send + 'static) { | ||||
| pub fn discover_agents( | ||||
|     timeout: Option<u64>, | ||||
|     on_event: impl Fn(DiscoveryEvent) -> Result<(), String> + Send + 'static, | ||||
| ) { | ||||
|     // Create a new mDNS daemon.
 | ||||
|     let mdns = ServiceDaemon::new().expect("Failed to create mDNS daemon"); | ||||
| 
 | ||||
| @ -14,13 +18,15 @@ pub fn discover_agents(timeout: Option<u64>, on_event: impl Fn(DiscoveryEvent) + | ||||
| 
 | ||||
|     std::thread::spawn(move || { | ||||
|         while let Ok(event) = receiver.recv() { | ||||
|             on_event(event.clone()); | ||||
|             if let Err(e) = on_event(event.clone()) { | ||||
|                 error!("Event callback failed : {e}"); | ||||
|             } | ||||
|             match event { | ||||
|                 ServiceEvent::ServiceResolved(resolved) => { | ||||
|                     println!("Resolved a new service: {}", resolved.fullname); | ||||
|                     debug!("Resolved a new service: {}", resolved.fullname); | ||||
|                 } | ||||
|                 other_event => { | ||||
|                     println!("Received other event: {:?}", &other_event); | ||||
|                     debug!("Received other event: {:?}", &other_event); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| use serde::Serialize; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize)] | ||||
| #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] | ||||
| pub struct MacAddress(pub [u8; 6]); | ||||
| 
 | ||||
| impl MacAddress { | ||||
| @ -25,6 +25,30 @@ impl std::fmt::Display for MacAddress { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl TryFrom<String> for MacAddress { | ||||
|     type Error = std::io::Error; | ||||
| 
 | ||||
|     fn try_from(value: String) -> Result<Self, Self::Error> { | ||||
|         let parts: Vec<&str> = value.split(':').collect(); | ||||
|         if parts.len() != 6 { | ||||
|             return Err(std::io::Error::new( | ||||
|                 std::io::ErrorKind::InvalidInput, | ||||
|                 "Invalid MAC address format: expected 6 colon-separated hex pairs", | ||||
|             )); | ||||
|         } | ||||
|         let mut bytes = [0u8; 6]; | ||||
|         for (i, part) in parts.iter().enumerate() { | ||||
|             bytes[i] = u8::from_str_radix(part, 16).map_err(|_| { | ||||
|                 std::io::Error::new( | ||||
|                     std::io::ErrorKind::InvalidInput, | ||||
|                     format!("Invalid hex value in part {}: '{}'", i, part), | ||||
|                 ) | ||||
|             })?; | ||||
|         } | ||||
|         Ok(MacAddress(bytes)) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub type IpAddress = std::net::IpAddr; | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user