feat/discover_inventory #127

Merged
letian merged 8 commits from feat/discover_inventory into refact/harmony_types 2025-08-31 22:45:09 +00:00
11 changed files with 135 additions and 41 deletions
Showing only changes of commit e548bf619a - Show all commits

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)
letian marked this conversation as resolved Outdated

pool is used so we shouldn't need the _

`pool` is used so we shouldn't need the `_`
.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
letian marked this conversation as resolved
Review

This log and the one above are very similar

This log and the one above are very similar
let host = PhysicalHost {
id: Id::from(host.host_uuid),
category: HostCategory::Server,
network: todo!(),
management: todo!(),
storage: todo!(),
labels: todo!(),
Review

It seems useless but I'm not sure they should actually be merged: to me it feels like two different concepts with a different lifecycle. It is highlighted by the fact that the harmony_inventory_agent is a standalone service that could be consumed by other programs and that this inventory module here is just one of them. The inventory has its own representation of a PhysicalHost that might become different than the one from the harmony_inventory_agent. At least it's my hypothesis for now.

It seems useless but I'm not sure they should actually be merged: to me it feels like two different concepts with a different lifecycle. It is highlighted by the fact that the `harmony_inventory_agent` is a standalone service that could be consumed by other programs and that this `inventory` module here is just one of them. The `inventory` has its own representation of a `PhysicalHost` that might become different than the one from the `harmony_inventory_agent`. At least it's my hypothesis for now.
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)]