feat: Can now discover inventory agent and download its host definition, next up save it to db

This commit is contained in:
Jean-Gabriel Gill-Couture 2025-08-30 20:01:52 -04:00
parent f9906cb419
commit e548bf619a
11 changed files with 135 additions and 41 deletions

3
Cargo.lock generated
View File

@ -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",

View File

@ -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 }

View File

@ -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,

View File

@ -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

View File

@ -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!()

View File

@ -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" }

View 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)
}

View File

@ -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")) {

View File

@ -1,2 +1,3 @@
mod hwinfo;
pub mod hwinfo;
pub mod local_presence;
pub mod client;

View File

@ -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);
}
}
}

View File

@ -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)]